From 768b9f08881e4e5bfbc36611bd48f833f125f7a5 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Fri, 25 Jul 2025 14:23:13 +0300 Subject: [PATCH 01/15] Adding a way to auto-populate table_name and field_name wildcards using values from DBs --- .../df-api-docs/df-api-docs.component.html | 16 ++++++++- .../df-api-docs/df-api-docs.component.ts | 35 ++++++++++++++++--- 2 files changed, 46 insertions(+), 5 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 9003c18f..a51e2b95 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 @@ -1,6 +1,7 @@
+ [class]="(isDarkMode | async) ? 'dark-theme' : ''" + style="display: flex; align-items: center; gap: 16px"> @@ -80,6 +81,19 @@ *ngIf="serviceName" [apiDocJson]="apiDocJson" [serviceName]="serviceName"> + +
+ + Populate table/field names in API docs + +
+ When enabled, the API documentation will include live table and field + names from your database. (May be slow for large databases) +
+
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 26a293fa..2998cf6a 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 @@ -45,11 +45,13 @@ import { distinctUntilChanged, catchError, } from 'rxjs/operators'; -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse, HttpBackend, HttpHeaders } from '@angular/common/http'; import { BASE_URL } from 'src/app/shared/constants/urls'; import { Subscription, of, forkJoin } from 'rxjs'; import { DfApiQuickstartComponent } from '../df-api-quickstart/df-api-quickstart.component'; import { ApiDocJson } from 'src/app/shared/types/files'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { FormsModule } from '@angular/forms'; interface ServiceResponse { resource: Array<{ @@ -88,6 +90,8 @@ interface HealthCheckResult { MatExpansionModule, MatCardModule, DfApiQuickstartComponent, + MatSlideToggleModule, + FormsModule, ], }) export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { @@ -103,6 +107,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { apiDocJson: ApiDocJson; apiKeys: ApiKeyInfo[] = []; faCopy = faCopy; + expandSchema = false; private subscriptions: Subscription[] = []; healthStatus: 'loading' | 'healthy' | 'unhealthy' | 'warning' = 'loading'; @@ -136,6 +141,8 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { ], }; + private rawHttp: HttpClient; + constructor( private activatedRoute: ActivatedRoute, private router: Router, @@ -145,14 +152,17 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { private clipboard: Clipboard, private snackBar: MatSnackBar, private currentServiceService: DfCurrentServiceService, - private http: HttpClient - ) {} + private http: HttpClient, + private httpBackend: HttpBackend + ) { + this.rawHttp = new HttpClient(httpBackend); + } isDarkMode = this.themeService.darkMode$; ngOnInit(): void { // Get the service name from the route this.serviceName = this.activatedRoute.snapshot.params['name']; - // First fetch the service ID by name + // First fetch the service ID by name (use normal http) if (this.serviceName) { this.subscriptions.push( this.http @@ -322,6 +332,23 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.showUnhealthyErrorDetails = !this.showUnhealthyErrorDetails; } + reloadApiDocs() { + if (!this.serviceName) return; + const params = this.expandSchema ? '?expand_schema=true' : ''; + const headers = new HttpHeaders({ + 'X-DreamFactory-API-Key': environment.dfApiDocsApiKey, + 'X-DreamFactory-Session-Token': this.userDataService.token || '' + }); + this.rawHttp + .get(`${BASE_URL}/api_docs/${this.serviceName}${params}`, { headers }) + .subscribe(data => { + if (data) { + this.apiDocJson = data; + } + this.ngAfterContentInit(); + }); + } + private injectCustomContent( swaggerContainer: HTMLElement, infoContainer: HTMLElement | null, From 7e3f14b905afaba961562d0117892854742645b6 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Fri, 25 Jul 2025 14:30:36 +0300 Subject: [PATCH 02/15] Adding a way to auto-populate table_name and field_name wildcards using values from DBs --- .../df-api-docs/df-api-docs.component.html | 7 ++++--- .../df-api-docs/df-api-docs.component.ts | 13 ++++++++++--- 2 files changed, 14 insertions(+), 6 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 a51e2b95..a74e5de6 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 @@ -82,11 +82,12 @@ [apiDocJson]="apiDocJson" [serviceName]="serviceName"> -
+
+ (ngModelChange)="reloadApiDocs()"> Populate table/field names in API docs
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 2998cf6a..9a261344 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 @@ -45,7 +45,12 @@ import { distinctUntilChanged, catchError, } from 'rxjs/operators'; -import { HttpClient, HttpErrorResponse, HttpBackend, HttpHeaders } from '@angular/common/http'; +import { + HttpClient, + HttpErrorResponse, + HttpBackend, + HttpHeaders, +} from '@angular/common/http'; import { BASE_URL } from 'src/app/shared/constants/urls'; import { Subscription, of, forkJoin } from 'rxjs'; import { DfApiQuickstartComponent } from '../df-api-quickstart/df-api-quickstart.component'; @@ -337,10 +342,12 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { const params = this.expandSchema ? '?expand_schema=true' : ''; const headers = new HttpHeaders({ 'X-DreamFactory-API-Key': environment.dfApiDocsApiKey, - 'X-DreamFactory-Session-Token': this.userDataService.token || '' + 'X-DreamFactory-Session-Token': this.userDataService.token || '', }); this.rawHttp - .get(`${BASE_URL}/api_docs/${this.serviceName}${params}`, { headers }) + .get(`${BASE_URL}/api_docs/${this.serviceName}${params}`, { + headers, + }) .subscribe(data => { if (data) { this.apiDocJson = data; From 0136f88efc474b4a1310d99dde8768e8a6f68646 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Tue, 29 Jul 2025 14:38:10 +0300 Subject: [PATCH 03/15] Moving step 4 into separate angular component, refactoring params handling and styles --- .../df-service-details.component.html | 141 +----- .../df-service-details.component.ts | 321 +------------ .../df-security-config.component.html | 52 ++ .../df-security-config.component.scss | 116 +++++ .../df-security-config.component.ts | 445 ++++++++++++++++++ 5 files changed, 621 insertions(+), 454 deletions(-) create mode 100644 src/app/shared/components/df-security-config/df-security-config.component.html create mode 100644 src/app/shared/components/df-security-config/df-security-config.component.scss create mode 100644 src/app/shared/components/df-security-config/df-security-config.component.ts diff --git a/src/app/adf-services/df-service-details/df-service-details.component.html b/src/app/adf-services/df-service-details/df-service-details.component.html index 62b448fd..64c47452 100644 --- a/src/app/adf-services/df-service-details/df-service-details.component.html +++ b/src/app/adf-services/df-service-details/df-service-details.component.html @@ -392,141 +392,12 @@

Security Configuration
-

Security Configuration

- -
-
-

- For more granular security options over your API check out the - Role Based Access - tab -

-
- - -
- - - -

Full Access

-

Grant complete access to all database components

-
-
-
- - - - -

Schema Access

-

Configure access to specific database schemas

-
-
-
- - - - -

Tables Access

-

Manage access to individual database tables

-
-
-
- - - - -

Stored Procedures

-

Control access to stored procedures

-
-
-
- - - - -

Functions

-

Set access levels for database functions

-
-
-
-
- - -
-

Security Configuration

-
-
- - -
-
-

Read Only

-

View access to data

-
-
-
- -
-
-

Read & Write

-

View and modify data

-
-
-
- -
-
-

Full Access

-

Complete control over data

-
-
-
-
-
-
-
-
- - -
- - -
+ +

diff --git a/src/app/adf-services/df-service-details/df-service-details.component.ts b/src/app/adf-services/df-service-details/df-service-details.component.ts index 58695482..b421deaf 100644 --- a/src/app/adf-services/df-service-details/df-service-details.component.ts +++ b/src/app/adf-services/df-service-details/df-service-details.component.ts @@ -27,6 +27,7 @@ import { TranslocoPipe } from '@ngneat/transloco'; import { DfArrayFieldComponent } from 'src/app/shared/components/df-field-array/df-array-field.component'; import { DfDynamicFieldComponent } from 'src/app/shared/components/df-dynamic-field/df-dynamic-field.component'; import { DfAceEditorComponent } from 'src/app/shared/components/df-ace-editor/df-ace-editor.component'; +import { DfSecurityConfigComponent } from 'src/app/shared/components/df-security-config/df-security-config.component'; import { ConfigSchema, ServiceType } from 'src/app/shared/types/service'; import { @@ -69,7 +70,6 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { DfThemeService } from 'src/app/shared/services/df-theme.service'; import { MatButtonToggleModule, - MatButtonToggleGroup, } from '@angular/material/button-toggle'; import { readAsText } from '../../shared/utilities/file'; import { DfSnackbarService } from 'src/app/shared/services/df-snackbar.service'; @@ -86,17 +86,8 @@ import { MatCardModule } from '@angular/material/card'; import { TitleCasePipe } from '@angular/common'; import { MatDividerModule } from '@angular/material/divider'; import { DfSystemService } from 'src/app/shared/services/df-system.service'; -import { DfUserDataService } from 'src/app/shared/services/df-user-data.service'; -import { DfPaywallService } from 'src/app/shared/services/df-paywall.service'; import { DfPaywallModal } from 'src/app/shared/components/df-paywall-modal/df-paywall-modal.component'; -// Add this interface before the @Component decorator -interface ComponentOption { - label: string; - value: string; - selected: boolean; -} - // Add these interfaces at the bottom of the file with the other interfaces interface RoleResponse { resource: Array<{ @@ -115,11 +106,6 @@ interface AppResponse { }>; } -interface CategorizedField { - basic: ConfigSchema[]; - advanced: ConfigSchema[]; -} - interface ServiceResponse { resource: Array<{ id: number; @@ -165,6 +151,7 @@ interface ServiceResponse { MatCardModule, TitleCasePipe, MatDividerModule, + DfSecurityConfigComponent, ], }) export class DfServiceDetailsComponent implements OnInit { @@ -188,15 +175,7 @@ export class DfServiceDetailsComponent implements OnInit { systemEvents: Array<{ label: string; value: string }>; content = ''; @ViewChild('stepper') stepper!: MatStepper; - @ViewChild('accessLevelGroup') accessLevelGroup!: MatButtonToggleGroup; showSecurityConfig = false; - selectedAccessType: string = ''; - selectedComponent: string = ''; - selectedAccessLevel: string = ''; - showComponentSelection = false; - componentList: ComponentOption[] = []; - componentSearch = ''; - selectedComponents: ComponentOption[] = []; currentServiceId: number | null = null; constructor( private activatedRoute: ActivatedRoute, @@ -824,121 +803,6 @@ export class DfServiceDetailsComponent implements OnInit { this.serviceDefinitionType = value; } - onAccessTypeChange(event: any) { - this.selectedComponent = ''; - this.showComponentSelection = this.selectedAccessType !== 'all'; - // Here you would typically load the appropriate components - // This is just example data - this.componentList = [ - { label: 'Component 1', value: 'comp1', selected: false }, - { label: 'Component 2', value: 'comp2', selected: false }, - { label: 'Component 3', value: 'comp3', selected: false }, - ]; - } - - onComponentSelect(component: any) { - // If component is a string (from dropdown/select) - if (typeof component === 'string') { - this.selectedComponent = component; - this.componentList.forEach(c => (c.selected = c.value === component)); - } - // If component is an object (from card selection) - else { - this.selectedComponent = component.value; - this.componentList.forEach( - c => (c.selected = c.value === component.value) - ); - } - } - - isSecurityConfigValid(): boolean { - const state = { - accessType: this.selectedAccessType, - accessLevel: this.selectedAccessLevel, - component: this.selectedComponent, - }; - - if (this.selectedAccessType === 'all') { - return ( - Boolean(this.selectedAccessLevel) && this.selectedComponent === '*' - ); - } - - return ( - Boolean(this.selectedAccessType) && - Boolean(this.selectedComponent) && - Boolean(this.selectedAccessLevel) && - this.selectedComponent.includes('/*') - ); // Ensure wildcard is present - } - - selectAccessType(type: string) { - this.selectedAccessType = type; - this.selectedComponent = ''; - this.showComponentSelection = type !== 'all'; - - if (type === 'all') { - this.componentList = []; - this.selectedComponent = '*'; // Set a default component for 'all' access - this.selectedAccessLevel = 'full'; // Automatically set full access for 'all' type - return; - } - - const serviceName = this.serviceForm.get('name')?.value; - switch (type) { - case 'tables': - this.selectedComponent = '_table/*'; - this.componentList = ['_table'].map(suffix => ({ - label: `${serviceName}${suffix}`, - value: `${suffix}/*`, - selected: true, - })); - break; - case 'procedures': - this.selectedComponent = '_proc/*'; - this.componentList = ['_proc'].map(suffix => ({ - label: `${serviceName}${suffix}`, - value: `${suffix}/*`, - selected: true, - })); - break; - case 'functions': - this.selectedComponent = '_func/*'; - this.componentList = ['_func'].map(suffix => ({ - label: `${serviceName}${suffix}`, - value: `${suffix}/*`, - selected: true, - })); - break; - default: - this.componentList = []; - } - } - - onAccessLevelChange(level: string) { - this.selectedAccessLevel = level; - } - - get filteredComponents() { - return this.componentList.filter(comp => - comp.label.toLowerCase().includes(this.componentSearch.toLowerCase()) - ); - } - - isComponentSelected(component: any) { - return component.selected; - } - - onComponentSelectionChange(component: any) { - if (component.selected) { - this.selectedComponents.push(component); - } else { - this.selectedComponents = this.selectedComponents.filter( - c => c.value !== component.value - ); - } - } - navigateToRoles(event: Event) { event.preventDefault(); // Navigate to roles tab @@ -1024,187 +888,6 @@ export class DfServiceDetailsComponent implements OnInit { } } - saveSecurityConfig() { - if (!this.isSecurityConfigValid()) { - return; - } - - if (!this.currentServiceId) { - this.snackBar.open('No service ID found. Please try again.', 'Close', { - duration: 3000, - }); - return; - } - - const serviceName = this.serviceForm.get('name')?.value; - const formattedName = this.formatServiceName(serviceName); - const roleName = `${serviceName}_auto_role`; - - const rolePayload = { - resource: [ - { - name: roleName, - description: `Auto-generated role for service ${serviceName}`, - is_active: true, - role_service_access_by_role_id: [ - { - service_id: this.currentServiceId, - component: this.selectedComponent, - verb_mask: this.getAccessLevel(this.selectedAccessLevel), - requestor_mask: 3, - filters: [], - filter_op: 'AND', - }, - ], - user_to_app_to_role_by_role_id: [], - }, - ], - }; - - // Create role and chain with app creation using proper RxJS operators - this.systemService - .post('role', rolePayload) - .pipe( - catchError(error => { - return throwError(() => error); - }), - switchMap((roleResponse: any) => { - if (!roleResponse?.resource?.[0]?.id) { - return throwError(() => new Error('Invalid role response')); - } - const createdRoleId = roleResponse.resource[0].id; - - const appPayload = { - resource: [ - { - name: `${serviceName}_app`, - description: `Auto-generated app for service ${serviceName}`, - type: '0', - role_id: createdRoleId, - is_active: true, - url: null, - storage_service_id: null, - storage_container: null, - path: null, - }, - ], - }; - - return this.systemService - .post('app?fields=*&related=role_by_role_id', appPayload) - .pipe( - catchError(error => { - this.snackBar.open( - `Error creating app: ${ - error.error?.message || error.message || 'Unknown error' - }`, - 'Close', - { duration: 5000 } - ); - return throwError(() => error); - }), - map((appResponse: any) => { - if (!appResponse?.resource?.[0]) { - throw new Error('App response missing resource array'); - } - - const app = appResponse.resource[0]; - - if (!app.apiKey) { - throw new Error('App response missing apiKey'); - } - - return { - apiKey: app.apiKey, - formattedName: formattedName, - }; - }), - catchError(error => { - return throwError(() => error); - }) - ); - }), - map((appResponse: any) => { - if (!appResponse?.apiKey) { - throw new Error('Invalid app response'); - } - return { - apiKey: appResponse.apiKey, - formattedName: formattedName, - }; - }) - ) - .subscribe({ - next: result => { - // Attempt to copy API key to clipboard - if (navigator.clipboard) { - navigator.clipboard - .writeText(result.apiKey) - .then(() => { - this.snackbarService.openSnackBar( - 'API Created and API Key copied to clipboard', - 'success' - ); - }) - .catch(() => { - this.snackbarService.openSnackBar( - 'API Created, but failed to copy API Key', - 'success' - ); - }); - } else { - this.snackbarService.openSnackBar( - 'API Created, but failed to copy API Key', - 'success' - ); - } - - // Navigate to API docs - this.router - .navigateByUrl( - `/api-connections/api-docs/${result.formattedName}`, - { - replaceUrl: true, - } - ) - .then(success => { - if (!success) { - this.router.navigate( - ['api-connections', 'api-docs', result.formattedName], - { - replaceUrl: true, - } - ); - } - }); - }, - error: error => { - // Show error message using DfSnackbarService - this.snackbarService.openSnackBar( - 'Error saving security configuration', - 'error' - ); - }, - }); - } - - private getAccessLevel(level: string): number { - switch (level) { - case 'read': - return 1; // GET - case 'write': - return 7; // GET (1) + POST (2) + PUT/PATCH (4) = 7 - case 'full': - return 15; // All permissions (GET + POST + PUT + DELETE) - default: - return 0; - } - } - - onAccessLevelSelect(level: string) { - this.selectedAccessLevel = level; - } - getServiceTypeLabel(value: string): string { const selectedType = this.serviceTypes.find(type => type.name === value); return selectedType ? selectedType.label : value; diff --git a/src/app/shared/components/df-security-config/df-security-config.component.html b/src/app/shared/components/df-security-config/df-security-config.component.html new file mode 100644 index 00000000..6a978091 --- /dev/null +++ b/src/app/shared/components/df-security-config/df-security-config.component.html @@ -0,0 +1,52 @@ +

+

Security Configuration

+ +
+ +
+
{{ option.label }}
+
{{ option.description }}
+
+ +
+ + Read Only + Read & Write + Full Access + +
+
+
+ +
+ + +
+ + +
+

Current Security Configurations ({{ securityConfigurations.length }}):

+
+

Configuration {{ i + 1 }}:

+

Access Type: {{ config.accessType }}

+

Access Level: {{ config.accessLevel }}

+

Component: {{ config.component }}

+
+

Valid: {{ isSecurityConfigValid() ? 'Yes' : 'No' }}

+
+
\ No newline at end of file diff --git a/src/app/shared/components/df-security-config/df-security-config.component.scss b/src/app/shared/components/df-security-config/df-security-config.component.scss new file mode 100644 index 00000000..8a8ffaf0 --- /dev/null +++ b/src/app/shared/components/df-security-config/df-security-config.component.scss @@ -0,0 +1,116 @@ +.security-config-wrapper { + padding: 24px; +} + +h3 { + margin-bottom: 16px; +} + +.security-cards-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.security-option-card { + padding: 16px; + cursor: pointer; + border: 2px solid transparent; + transition: border 0.2s ease, box-shadow 0.2s ease; + + &.selected { + border-color: #1976d2; + background-color: #e3f2fd; + box-shadow: 0 4px 8px rgba(25, 118, 210, 0.2); + } + + &:hover { + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .card-header { + margin-bottom: 12px; + } + + .card-title { + font-weight: 600; + font-size: 16px; + margin-bottom: 4px; + } + + .card-description { + font-size: 13px; + color: #777; + line-height: 1.4; + } + + .toggle-container { + margin-top: 12px; + } + + .access-toggle-group { + display: flex; + flex-wrap: wrap; + gap: 4px; + + .mat-button-toggle { + font-size: 12px; + padding: 4px 8px; + min-width: auto; + } + } +} + +.action-buttons { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid #e0e0e0; +} + +.debug-info { + margin-top: 24px; + padding: 16px; + background-color: #f5f5f5; + border-radius: 4px; + border-left: 4px solid #1976d2; + + h4 { + margin: 0 0 12px 0; + color: #1976d2; + font-size: 14px; + font-weight: 600; + } + + p { + margin: 4px 0; + font-size: 13px; + color: #333; + + strong { + color: #555; + } + } + + .config-item { + margin: 12px 0; + padding: 8px; + background-color: white; + border-radius: 4px; + border: 1px solid #e0e0e0; + + p { + margin: 2px 0; + font-size: 12px; + } + + p:first-child { + font-weight: 600; + color: #1976d2; + margin-bottom: 6px; + } + } +} diff --git a/src/app/shared/components/df-security-config/df-security-config.component.ts b/src/app/shared/components/df-security-config/df-security-config.component.ts new file mode 100644 index 00000000..2f39ad2c --- /dev/null +++ b/src/app/shared/components/df-security-config/df-security-config.component.ts @@ -0,0 +1,445 @@ +import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { Router } from '@angular/router'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { DfSystemService } from 'src/app/shared/services/df-system.service'; +import { DfSnackbarService } from 'src/app/shared/services/df-snackbar.service'; +import { switchMap, catchError, map } from 'rxjs'; +import { throwError } from 'rxjs'; + +interface AccessOption { + key: string; + label: string; + description: string; + selected: boolean; + level: 'read' | 'write' | 'full'; +} + +interface SecurityConfigData { + accessType: string; + accessLevel: string; + component: string; +} + +@Component({ + selector: 'df-security-config', + standalone: true, + templateUrl: './df-security-config.component.html', + styleUrls: ['./df-security-config.component.scss'], + imports: [ + CommonModule, + FormsModule, + MatCardModule, + MatButtonToggleModule, + MatButtonModule, + MatCheckboxModule + ] +}) +export class DfSecurityConfigComponent implements OnInit { + @Input() serviceName: string = ''; + @Input() serviceId: number | null = null; + @Input() isDatabase: boolean = false; + + @Output() goBack = new EventEmitter(); + + // Multiple security configurations + securityConfigurations: SecurityConfigData[] = []; + + // Access options for the component design + accessOptions: AccessOption[] = []; + + constructor( + private router: Router, + private snackBar: MatSnackBar, + private systemService: DfSystemService, + private snackbarService: DfSnackbarService + ) {} + + ngOnInit(): void { + this.initializeAccessOptions(); + } + + private initializeAccessOptions(): void { + this.accessOptions = [ + { + key: 'fullAccess', + label: 'Full Access', + description: 'Grant complete access to all database components', + selected: false, + level: 'read' + }, + { + key: 'schemaAccess', + label: 'Schema Access', + description: 'Configure access to specific database schemas', + selected: false, + level: 'read' + }, + { + key: 'tableAccess', + label: 'Table Access', + description: 'Manage access to individual database tables', + selected: false, + level: 'read' + }, + { + key: 'storedProcedures', + label: 'Stored Procedures', + description: 'Control access to stored procedures', + selected: false, + level: 'read' + }, + { + key: 'functions', + label: 'Functions', + description: 'Set access levels for database functions', + selected: false, + level: 'read' + } + ]; + } + + // Original component methods + toggleCard(option: AccessOption): void { + option.selected = !option.selected; + + // Update the security configuration based on the selected option + if (option.selected) { + // Add new configuration + this.addSecurityConfiguration(option); + } else { + // Remove configuration for this option + this.removeSecurityConfiguration(option.key); + } + } + + private addSecurityConfiguration(option: AccessOption): void { + // Map the access option key to the corresponding access type + let accessType = ''; + let component = ''; + + switch (option.key) { + case 'fullAccess': + accessType = 'all'; + component = '*'; + break; + case 'schemaAccess': + accessType = 'schema'; + component = '_schema/*'; + break; + case 'tableAccess': + accessType = 'tables'; + component = '_table/*'; + break; + case 'storedProcedures': + accessType = 'procedures'; + component = '_proc/*'; + break; + case 'functions': + accessType = 'functions'; + component = '_func/*'; + break; + } + + // Create new security configuration + const newConfig: SecurityConfigData = { + accessType: accessType, + accessLevel: option.level, + component: component + }; + + // Add to configurations array + this.securityConfigurations.push(newConfig); + + console.log('Added security configuration:', newConfig); + console.log('All configurations:', this.securityConfigurations); + } + + private removeSecurityConfiguration(optionKey: string): void { + // Find and remove the configuration for this option + const index = this.securityConfigurations.findIndex(config => { + switch (optionKey) { + case 'fullAccess': + return config.accessType === 'all'; + case 'schemaAccess': + return config.accessType === 'schema'; + case 'tableAccess': + return config.accessType === 'tables'; + case 'storedProcedures': + return config.accessType === 'procedures'; + case 'functions': + return config.accessType === 'functions'; + default: + return false; + } + }); + + if (index !== -1) { + const removed = this.securityConfigurations.splice(index, 1)[0]; + console.log('Removed security configuration:', removed); + console.log('Remaining configurations:', this.securityConfigurations); + } + } + + onAccessLevelChange(option: AccessOption, level: 'read' | 'write' | 'full'): void { + option.level = level; + + // Update the corresponding configuration in the array + const configIndex = this.securityConfigurations.findIndex(config => { + switch (option.key) { + case 'fullAccess': + return config.accessType === 'all'; + case 'schemaAccess': + return config.accessType === 'schema'; + case 'tableAccess': + return config.accessType === 'tables'; + case 'storedProcedures': + return config.accessType === 'procedures'; + case 'functions': + return config.accessType === 'functions'; + default: + return false; + } + }); + + if (configIndex !== -1) { + this.securityConfigurations[configIndex].accessLevel = level; + console.log('Updated access level for configuration:', this.securityConfigurations[configIndex]); + } + } + + handleGoBack(): void { + console.log('Back button clicked'); + this.goBack.emit(); + } + + isSecurityConfigValid(): boolean { + // Check if at least one option is selected + const hasSelectedOption = this.accessOptions.some(opt => opt.selected); + + if (!hasSelectedOption) { + return false; + } + + // Check if we have configurations + if (this.securityConfigurations.length === 0) { + return false; + } + + // Validate each configuration + for (const config of this.securityConfigurations) { + if (!config.accessType || !config.accessLevel || !config.component) { + return false; + } + + // Additional validation based on access type + if (config.accessType === 'all') { + if (config.component !== '*') { + return false; + } + } else { + // For other access types, ensure component has wildcard + if (!config.component.includes('/*')) { + return false; + } + } + } + + return true; + } + + saveSecurityConfig() { + if (!this.isSecurityConfigValid()) { + this.snackbarService.openSnackBar( + 'Please select at least one access option and ensure all required fields are filled', + 'error' + ); + return; + } + + if (!this.serviceId) { + this.snackBar.open('No service ID found. Please try again.', 'Close', { + duration: 3000, + }); + return; + } + + const formattedName = this.formatServiceName(this.serviceName); + const roleName = `${this.serviceName}_auto_role`; + + // Create role service access entries for each configuration + const roleServiceAccessEntries = this.securityConfigurations.map(config => ({ + service_id: this.serviceId, + component: config.component, + verb_mask: this.getAccessLevel(config.accessLevel), + requestor_mask: 3, + filters: [], + filter_op: 'AND', + })); + + const rolePayload = { + resource: [ + { + name: roleName, + description: `Auto-generated role for service ${this.serviceName}`, + is_active: true, + role_service_access_by_role_id: roleServiceAccessEntries, + user_to_app_to_role_by_role_id: [], + }, + ], + }; + + console.log('Creating role with multiple configurations:', rolePayload); + + // Create role and chain with app creation using proper RxJS operators + this.systemService + .post('role', rolePayload) + .pipe( + catchError(error => { + return throwError(() => error); + }), + switchMap((roleResponse: any) => { + if (!roleResponse?.resource?.[0]?.id) { + return throwError(() => new Error('Invalid role response')); + } + const createdRoleId = roleResponse.resource[0].id; + + const appPayload = { + resource: [ + { + name: `${this.serviceName}_app`, + description: `Auto-generated app for service ${this.serviceName}`, + type: '0', + role_id: createdRoleId, + is_active: true, + url: null, + storage_service_id: null, + storage_container: null, + path: null, + }, + ], + }; + + return this.systemService + .post('app?fields=*&related=role_by_role_id', appPayload) + .pipe( + catchError(error => { + this.snackBar.open( + `Error creating app: ${ + error.error?.message || error.message || 'Unknown error' + }`, + 'Close', + { duration: 5000 } + ); + return throwError(() => error); + }), + map((appResponse: any) => { + if (!appResponse?.resource?.[0]) { + throw new Error('App response missing resource array'); + } + + const app = appResponse.resource[0]; + + if (!app.apiKey) { + throw new Error('App response missing apiKey'); + } + + return { + apiKey: app.apiKey, + formattedName: formattedName, + }; + }), + catchError(error => { + return throwError(() => error); + }) + ); + }), + map((appResponse: any) => { + if (!appResponse?.apiKey) { + throw new Error('Invalid app response'); + } + return { + apiKey: appResponse.apiKey, + formattedName: formattedName, + }; + }) + ) + .subscribe({ + next: result => { + // Attempt to copy API key to clipboard + if (navigator.clipboard) { + navigator.clipboard + .writeText(result.apiKey) + .then(() => { + this.snackbarService.openSnackBar( + `API Created with ${this.securityConfigurations.length} security configuration(s) and API Key copied to clipboard`, + 'success' + ); + }) + .catch(() => { + this.snackbarService.openSnackBar( + `API Created with ${this.securityConfigurations.length} security configuration(s), but failed to copy API Key`, + 'success' + ); + }); + } else { + this.snackbarService.openSnackBar( + `API Created with ${this.securityConfigurations.length} security configuration(s), but failed to copy API Key`, + 'success' + ); + } + + // Navigate to API docs + this.router + .navigateByUrl( + `/api-connections/api-docs/${result.formattedName}`, + { + replaceUrl: true, + } + ) + .then(success => { + if (!success) { + this.router.navigate( + ['api-connections', 'api-docs', result.formattedName], + { + replaceUrl: true, + } + ); + } + }); + }, + error: error => { + // Show error message using DfSnackbarService + this.snackbarService.openSnackBar( + 'Error saving security configuration', + 'error' + ); + }, + }); + } + + private getAccessLevel(level: string): number { + switch (level) { + case 'read': + return 1; // GET + case 'write': + return 7; // GET (1) + POST (2) + PUT/PATCH (4) = 7 + case 'full': + return 15; // All permissions (GET + POST + PUT + DELETE) + default: + return 0; + } + } + + private formatServiceName(name: string): string { + return name + .toLowerCase() + .replace(/\s+/g, '') + .replace(/[^a-z0-9_-]/g, ''); + } +} From f3012de8e065dfbe71baf1423842386f3f7cec8c Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Tue, 29 Jul 2025 14:38:35 +0300 Subject: [PATCH 04/15] Moving step 4 into separate angular component, refactoring params handling and styles --- .../df-service-details.component.ts | 4 +- .../df-security-config.component.html | 20 +++--- .../df-security-config.component.scss | 4 +- .../df-security-config.component.ts | 62 +++++++++++-------- 4 files changed, 52 insertions(+), 38 deletions(-) diff --git a/src/app/adf-services/df-service-details/df-service-details.component.ts b/src/app/adf-services/df-service-details/df-service-details.component.ts index b421deaf..e6dfb191 100644 --- a/src/app/adf-services/df-service-details/df-service-details.component.ts +++ b/src/app/adf-services/df-service-details/df-service-details.component.ts @@ -68,9 +68,7 @@ import { MatIconModule } from '@angular/material/icon'; import { HttpClient } from '@angular/common/http'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { DfThemeService } from 'src/app/shared/services/df-theme.service'; -import { - MatButtonToggleModule, -} from '@angular/material/button-toggle'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { readAsText } from '../../shared/utilities/file'; import { DfSnackbarService } from 'src/app/shared/services/df-snackbar.service'; import { BASE_URL } from 'src/app/shared/constants/urls'; diff --git a/src/app/shared/components/df-security-config/df-security-config.component.html b/src/app/shared/components/df-security-config/df-security-config.component.html index 6a978091..9b3a1d6a 100644 --- a/src/app/shared/components/df-security-config/df-security-config.component.html +++ b/src/app/shared/components/df-security-config/df-security-config.component.html @@ -29,9 +29,9 @@

Security Configuration

-
diff --git a/src/app/shared/components/df-security-config/df-security-config.component.scss b/src/app/shared/components/df-security-config/df-security-config.component.scss index 8a8ffaf0..5677db67 100644 --- a/src/app/shared/components/df-security-config/df-security-config.component.scss +++ b/src/app/shared/components/df-security-config/df-security-config.component.scss @@ -17,7 +17,9 @@ h3 { padding: 16px; cursor: pointer; border: 2px solid transparent; - transition: border 0.2s ease, box-shadow 0.2s ease; + transition: + border 0.2s ease, + box-shadow 0.2s ease; &.selected { border-color: #1976d2; diff --git a/src/app/shared/components/df-security-config/df-security-config.component.ts b/src/app/shared/components/df-security-config/df-security-config.component.ts index 2f39ad2c..395cd4c9 100644 --- a/src/app/shared/components/df-security-config/df-security-config.component.ts +++ b/src/app/shared/components/df-security-config/df-security-config.component.ts @@ -37,19 +37,19 @@ interface SecurityConfigData { MatCardModule, MatButtonToggleModule, MatButtonModule, - MatCheckboxModule - ] + MatCheckboxModule, + ], }) export class DfSecurityConfigComponent implements OnInit { @Input() serviceName: string = ''; @Input() serviceId: number | null = null; @Input() isDatabase: boolean = false; - + @Output() goBack = new EventEmitter(); - + // Multiple security configurations securityConfigurations: SecurityConfigData[] = []; - + // Access options for the component design accessOptions: AccessOption[] = []; @@ -71,43 +71,43 @@ export class DfSecurityConfigComponent implements OnInit { label: 'Full Access', description: 'Grant complete access to all database components', selected: false, - level: 'read' + level: 'read', }, { key: 'schemaAccess', label: 'Schema Access', description: 'Configure access to specific database schemas', selected: false, - level: 'read' + level: 'read', }, { key: 'tableAccess', label: 'Table Access', description: 'Manage access to individual database tables', selected: false, - level: 'read' + level: 'read', }, { key: 'storedProcedures', label: 'Stored Procedures', description: 'Control access to stored procedures', selected: false, - level: 'read' + level: 'read', }, { key: 'functions', label: 'Functions', description: 'Set access levels for database functions', selected: false, - level: 'read' - } + level: 'read', + }, ]; } // Original component methods toggleCard(option: AccessOption): void { option.selected = !option.selected; - + // Update the security configuration based on the selected option if (option.selected) { // Add new configuration @@ -122,7 +122,7 @@ export class DfSecurityConfigComponent implements OnInit { // Map the access option key to the corresponding access type let accessType = ''; let component = ''; - + switch (option.key) { case 'fullAccess': accessType = 'all'; @@ -150,12 +150,12 @@ export class DfSecurityConfigComponent implements OnInit { const newConfig: SecurityConfigData = { accessType: accessType, accessLevel: option.level, - component: component + component: component, }; // Add to configurations array this.securityConfigurations.push(newConfig); - + console.log('Added security configuration:', newConfig); console.log('All configurations:', this.securityConfigurations); } @@ -186,9 +186,12 @@ export class DfSecurityConfigComponent implements OnInit { } } - onAccessLevelChange(option: AccessOption, level: 'read' | 'write' | 'full'): void { + onAccessLevelChange( + option: AccessOption, + level: 'read' | 'write' | 'full' + ): void { option.level = level; - + // Update the corresponding configuration in the array const configIndex = this.securityConfigurations.findIndex(config => { switch (option.key) { @@ -209,7 +212,10 @@ export class DfSecurityConfigComponent implements OnInit { if (configIndex !== -1) { this.securityConfigurations[configIndex].accessLevel = level; - console.log('Updated access level for configuration:', this.securityConfigurations[configIndex]); + console.log( + 'Updated access level for configuration:', + this.securityConfigurations[configIndex] + ); } } @@ -221,7 +227,7 @@ export class DfSecurityConfigComponent implements OnInit { isSecurityConfigValid(): boolean { // Check if at least one option is selected const hasSelectedOption = this.accessOptions.some(opt => opt.selected); - + if (!hasSelectedOption) { return false; } @@ -273,14 +279,16 @@ export class DfSecurityConfigComponent implements OnInit { const roleName = `${this.serviceName}_auto_role`; // Create role service access entries for each configuration - const roleServiceAccessEntries = this.securityConfigurations.map(config => ({ - service_id: this.serviceId, - component: config.component, - verb_mask: this.getAccessLevel(config.accessLevel), - requestor_mask: 3, - filters: [], - filter_op: 'AND', - })); + const roleServiceAccessEntries = this.securityConfigurations.map( + config => ({ + service_id: this.serviceId, + component: config.component, + verb_mask: this.getAccessLevel(config.accessLevel), + requestor_mask: 3, + filters: [], + filter_op: 'AND', + }) + ); const rolePayload = { resource: [ From ca1ea99664a806f5e26af56bae0ab5d7325bd818 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Wed, 30 Jul 2025 11:10:07 +0300 Subject: [PATCH 05/15] Ensure other access types are unset when full access card is selected --- .../df-security-config.component.html | 46 ++-- .../df-security-config.component.scss | 213 +++++++++++------- .../df-security-config.component.ts | 30 +++ 3 files changed, 182 insertions(+), 107 deletions(-) diff --git a/src/app/shared/components/df-security-config/df-security-config.component.html b/src/app/shared/components/df-security-config/df-security-config.component.html index 9b3a1d6a..31c50971 100644 --- a/src/app/shared/components/df-security-config/df-security-config.component.html +++ b/src/app/shared/components/df-security-config/df-security-config.component.html @@ -1,10 +1,13 @@

Security Configuration

-
+
@@ -12,16 +15,28 @@

Security Configuration

{{ option.description }}
-
+
- Read Only - Read & Write - Full Access + + + Read Only + + + + + Read & Write + + + + + Full Access +
@@ -32,27 +47,10 @@

Security Configuration

- - -
-

- Current Security Configurations ({{ securityConfigurations.length }}): -

-
-

- Configuration {{ i + 1 }}: -

-

Access Type: {{ config.accessType }}

-

Access Level: {{ config.accessLevel }}

-

Component: {{ config.component }}

-
-

Valid: {{ isSecurityConfigValid() ? 'Yes' : 'No' }}

-
diff --git a/src/app/shared/components/df-security-config/df-security-config.component.scss b/src/app/shared/components/df-security-config/df-security-config.component.scss index 5677db67..1f2d2e7c 100644 --- a/src/app/shared/components/df-security-config/df-security-config.component.scss +++ b/src/app/shared/components/df-security-config/df-security-config.component.scss @@ -1,65 +1,156 @@ .security-config-wrapper { padding: 24px; -} + max-width: 1200px; + margin: 0 auto; -h3 { - margin-bottom: 16px; + h3 { + margin-bottom: 24px; + font-size: 24px; + font-weight: 600; + color: #1976d2; + text-align: center; + } } -.security-cards-grid { +.security-cards-container { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 16px; - margin-bottom: 24px; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 20px; + margin-bottom: 32px; + + // Responsive breakpoints + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: 16px; + } + + @media (min-width: 769px) and (max-width: 1024px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 1025px) { + grid-template-columns: repeat(3, 1fr); + } } .security-option-card { - padding: 16px; + padding: 20px; cursor: pointer; - border: 2px solid transparent; - transition: - border 0.2s ease, - box-shadow 0.2s ease; + border: 2px solid #e0e0e0; + border-radius: 12px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + position: relative; + overflow: hidden; - &.selected { + &:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); border-color: #1976d2; - background-color: #e3f2fd; - box-shadow: 0 4px 8px rgba(25, 118, 210, 0.2); } - &:hover { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + &.selected { + border-color: #1976d2; + box-shadow: 0 4px 20px rgba(25, 118, 210, 0.15); + + &.read-level { + border-color: #2196f3; + background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); + } + + &.write-level { + border-color: #fbc02d; + background: linear-gradient(135deg, #fffde7 0%, #fff9c4 100%); + } + + &.full-level { + border-color: #43a047; + background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%); + } } .card-header { - margin-bottom: 12px; - } + margin-bottom: 16px; - .card-title { - font-weight: 600; - font-size: 16px; - margin-bottom: 4px; - } + .card-title { + font-weight: 600; + font-size: 18px; + margin-bottom: 8px; + color: #333; + } - .card-description { - font-size: 13px; - color: #777; - line-height: 1.4; + .card-description { + font-size: 14px; + color: #666; + line-height: 1.5; + } } .toggle-container { - margin-top: 12px; - } - - .access-toggle-group { - display: flex; - flex-wrap: wrap; - gap: 4px; - - .mat-button-toggle { + .access-level-label { font-size: 12px; - padding: 4px 8px; - min-width: auto; + font-weight: 600; + color: #666; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .access-toggle-group { + display: flex; + flex-wrap: wrap; + gap: 4px; + box-shadow: none; + + .mat-button-toggle-checked { + color: #666; + } + .mat-button-toggle { + background-color: #EEEEEE; + font-size: 12px; + padding: 6px 12px; + width: 100%; + border-radius: 6px; + transition: all 0.2s ease; + // Responsive breakpoints + @media (max-width: 768px) { + width: 150px; + } + + &.mat-button-toggle-disabled { + opacity: 0.5; + pointer-events: none; + background-color: #f5f5f5; + color: #999; + border-color: #ddd; + } + + .toggle-icon { + margin-right: 4px; + font-size: 14px; + } + + &.read-toggle { + &.mat-button-toggle-checked { + background-color: #2196f3; + color: white; + } + } + + &.write-toggle { + &.mat-button-toggle-checked { + background-color: #fbc02d; + color: white; + } + } + + &.full-toggle { + &.mat-button-toggle-checked { + background-color: #43a047; + color: white; + } + } + } } } } @@ -72,47 +163,3 @@ h3 { padding-top: 16px; border-top: 1px solid #e0e0e0; } - -.debug-info { - margin-top: 24px; - padding: 16px; - background-color: #f5f5f5; - border-radius: 4px; - border-left: 4px solid #1976d2; - - h4 { - margin: 0 0 12px 0; - color: #1976d2; - font-size: 14px; - font-weight: 600; - } - - p { - margin: 4px 0; - font-size: 13px; - color: #333; - - strong { - color: #555; - } - } - - .config-item { - margin: 12px 0; - padding: 8px; - background-color: white; - border-radius: 4px; - border: 1px solid #e0e0e0; - - p { - margin: 2px 0; - font-size: 12px; - } - - p:first-child { - font-weight: 600; - color: #1976d2; - margin-bottom: 6px; - } - } -} diff --git a/src/app/shared/components/df-security-config/df-security-config.component.ts b/src/app/shared/components/df-security-config/df-security-config.component.ts index 395cd4c9..072d91c4 100644 --- a/src/app/shared/components/df-security-config/df-security-config.component.ts +++ b/src/app/shared/components/df-security-config/df-security-config.component.ts @@ -5,12 +5,15 @@ import { MatCardModule } from '@angular/material/card'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; import { Router } from '@angular/router'; import { MatSnackBar } from '@angular/material/snack-bar'; import { DfSystemService } from 'src/app/shared/services/df-system.service'; import { DfSnackbarService } from 'src/app/shared/services/df-snackbar.service'; import { switchMap, catchError, map } from 'rxjs'; import { throwError } from 'rxjs'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faEye, faPen, faLockOpen } from '@fortawesome/free-solid-svg-icons'; interface AccessOption { key: string; @@ -38,6 +41,8 @@ interface SecurityConfigData { MatButtonToggleModule, MatButtonModule, MatCheckboxModule, + MatIconModule, + FontAwesomeModule, ], }) export class DfSecurityConfigComponent implements OnInit { @@ -47,6 +52,11 @@ export class DfSecurityConfigComponent implements OnInit { @Output() goBack = new EventEmitter(); + // FontAwesome icons + faEye = faEye; + faPen = faPen; + faLockOpen = faLockOpen; + // Multiple security configurations securityConfigurations: SecurityConfigData[] = []; @@ -106,6 +116,26 @@ export class DfSecurityConfigComponent implements OnInit { // Original component methods toggleCard(option: AccessOption): void { + // Special handling for full access + if (option.key === 'fullAccess') { + if (!option.selected) { + // When selecting full access, unselect all other options + this.accessOptions.forEach(opt => { + if (opt.key !== 'fullAccess' && opt.selected) { + opt.selected = false; + this.removeSecurityConfiguration(opt.key); + } + }); + } + } else { + // For other options, if full access is selected, unselect it first + const fullAccessOption = this.accessOptions.find(opt => opt.key === 'fullAccess'); + if (fullAccessOption && fullAccessOption.selected) { + fullAccessOption.selected = false; + this.removeSecurityConfiguration(fullAccessOption.key); + } + } + option.selected = !option.selected; // Update the security configuration based on the selected option From 58006182a31848f3e4b822effcd080cc1566e0c7 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Wed, 30 Jul 2025 11:22:30 +0300 Subject: [PATCH 06/15] Used a prettier --- .../df-security-config/df-security-config.component.html | 4 +++- .../df-security-config/df-security-config.component.scss | 2 +- .../df-security-config/df-security-config.component.ts | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/shared/components/df-security-config/df-security-config.component.html b/src/app/shared/components/df-security-config/df-security-config.component.html index 31c50971..12ea5634 100644 --- a/src/app/shared/components/df-security-config/df-security-config.component.html +++ b/src/app/shared/components/df-security-config/df-security-config.component.html @@ -34,7 +34,9 @@

Security Configuration

- + Full Access diff --git a/src/app/shared/components/df-security-config/df-security-config.component.scss b/src/app/shared/components/df-security-config/df-security-config.component.scss index 1f2d2e7c..af1f509a 100644 --- a/src/app/shared/components/df-security-config/df-security-config.component.scss +++ b/src/app/shared/components/df-security-config/df-security-config.component.scss @@ -106,7 +106,7 @@ color: #666; } .mat-button-toggle { - background-color: #EEEEEE; + background-color: #eeeeee; font-size: 12px; padding: 6px 12px; width: 100%; diff --git a/src/app/shared/components/df-security-config/df-security-config.component.ts b/src/app/shared/components/df-security-config/df-security-config.component.ts index 072d91c4..906a7487 100644 --- a/src/app/shared/components/df-security-config/df-security-config.component.ts +++ b/src/app/shared/components/df-security-config/df-security-config.component.ts @@ -129,7 +129,9 @@ export class DfSecurityConfigComponent implements OnInit { } } else { // For other options, if full access is selected, unselect it first - const fullAccessOption = this.accessOptions.find(opt => opt.key === 'fullAccess'); + const fullAccessOption = this.accessOptions.find( + opt => opt.key === 'fullAccess' + ); if (fullAccessOption && fullAccessOption.selected) { fullAccessOption.selected = false; this.removeSecurityConfiguration(fullAccessOption.key); From 3017a3ac2b0e641d1a0fe6a10d6061609d69d051 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Wed, 30 Jul 2025 13:30:13 +0300 Subject: [PATCH 07/15] Provide a way to test API keys via API docs UI using Available API key select component --- .../df-api-docs/df-api-docs.component.html | 15 ++- .../df-api-docs/df-api-docs.component.ts | 103 ++++++++++-------- .../df-api-quickstart.component.ts | 17 ++- 3 files changed, 86 insertions(+), 49 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 9003c18f..c839930a 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 @@ -14,7 +14,17 @@
{{ 'apiDocs.apiKeys.label' | transloco }} - + + +
+
+ None (Session token based authentication) + Uses session token to build the request +
+
+
@@ -79,7 +89,8 @@ + [serviceName]="serviceName" + [selectedApiKey]="selectedApiKey">
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 26a293fa..036793ee 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 @@ -13,6 +13,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatIconModule } from '@angular/material/icon'; import { TranslocoModule } from '@ngneat/transloco'; +import { FormsModule } from '@angular/forms'; import { saveRawAsFile } from 'src/app/shared/utilities/file'; import { UntilDestroy } from '@ngneat/until-destroy'; import { DfUserDataService } from 'src/app/shared/services/df-user-data.service'; @@ -77,6 +78,7 @@ interface HealthCheckResult { MatSelectModule, MatIconModule, TranslocoModule, + FormsModule, AsyncPipe, NgIf, NgFor, @@ -102,6 +104,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { apiDocJson: ApiDocJson; apiKeys: ApiKeyInfo[] = []; + selectedApiKey: string | null = null; faCopy = faCopy; private subscriptions: Subscription[] = []; @@ -201,51 +204,8 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { } ngAfterContentInit(): void { - const apiDocumentation = this.apiDocJson; this.checkApiHealth(); - - SwaggerUI({ - spec: apiDocumentation, - domNode: this.apiDocElement?.nativeElement, - requestInterceptor: (req: SwaggerUI.Request) => { - req['headers'][SESSION_TOKEN_HEADER] = this.userDataService.token; - req['headers'][API_KEY_HEADER] = environment.dfApiDocsApiKey; - // Parse the request URL - const url = new URL(req['url']); - const params = new URLSearchParams(url.search); - // Decode all parameters - params.forEach((value, key) => { - params.set(key, decodeURIComponent(value)); - }); - // Update the URL with decoded parameters - url.search = params.toString(); - req['url'] = url.toString(); - return req; - }, - showMutatedRequest: true, - onComplete: () => { - if ( - this.apiDocElement && - this.apiDocElement.nativeElement && - this.swaggerInjectedContentContainerRef && - this.swaggerInjectedContentContainerRef.nativeElement - ) { - const swaggerContainer = this.apiDocElement.nativeElement; - const customContentNode = - this.swaggerInjectedContentContainerRef.nativeElement; - - const infoContainer = swaggerContainer.querySelector( - '.information-container .main' - ); - - this.injectCustomContent( - swaggerContainer, - infoContainer, - customContentNode - ); - } - }, - }); + this.generateSwaggerWithApiKey(this.apiDocJson); } ngOnDestroy(): void { @@ -322,6 +282,61 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.showUnhealthyErrorDetails = !this.showUnhealthyErrorDetails; } + onApiKeySelectionChange(selectedKey: string | null): void { + this.selectedApiKey = selectedKey; + // Regenerate Swagger documentation with the new API key (or null for session token) + this.generateSwaggerWithApiKey(this.apiDocJson); + } + + private generateSwaggerWithApiKey(apiDocumentation: ApiDocJson): void { + SwaggerUI({ + spec: apiDocumentation, + domNode: this.apiDocElement?.nativeElement, + requestInterceptor: (req: SwaggerUI.Request) => { + if (this.selectedApiKey == null) { + req['headers'][SESSION_TOKEN_HEADER] = this.userDataService.token; + } + // Use selected API key if available, otherwise fall back to environment key + const apiKey = this.selectedApiKey || environment.dfApiDocsApiKey; + req['headers'][API_KEY_HEADER] = apiKey; + // Parse the request URL + const url = new URL(req['url']); + const params = new URLSearchParams(url.search); + // Decode all parameters + params.forEach((value, key) => { + params.set(key, decodeURIComponent(value)); + }); + // Update the URL with decoded parameters + url.search = params.toString(); + req['url'] = url.toString(); + return req; + }, + showMutatedRequest: true, + onComplete: () => { + if ( + this.apiDocElement && + this.apiDocElement.nativeElement && + this.swaggerInjectedContentContainerRef && + this.swaggerInjectedContentContainerRef.nativeElement + ) { + const swaggerContainer = this.apiDocElement.nativeElement; + const customContentNode = + this.swaggerInjectedContentContainerRef.nativeElement; + + const infoContainer = swaggerContainer.querySelector( + '.information-container .main' + ); + + this.injectCustomContent( + swaggerContainer, + infoContainer, + customContentNode + ); + } + }, + }); + } + private injectCustomContent( swaggerContainer: HTMLElement, infoContainer: HTMLElement | null, diff --git a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts index 6a97d20e..a46da826 100644 --- a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts @@ -15,6 +15,7 @@ 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'; +import { API_KEY_HEADER } from 'src/app/shared/constants/http-headers'; interface CurlCommand { title: string; @@ -70,6 +71,7 @@ const healthCheckEndpointsInfo: { export class DfApiQuickstartComponent implements OnChanges { @Input() apiDocJson: ApiDocJson; @Input() serviceName: string; + @Input() selectedApiKey: string | null = null; curlCommands: CurlCommand[] = []; faCopy = faCopy; @@ -82,7 +84,9 @@ export class DfApiQuickstartComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if ( - (changes['apiDocJson'] || changes['serviceName']) && + (changes['apiDocJson'] || + changes['serviceName'] || + changes['selectedApiKey']) && this.apiDocJson && this.serviceName ) { @@ -103,9 +107,16 @@ export class DfApiQuickstartComponent implements OnChanges { const endpointsInfo = healthCheckEndpointsInfo[this.apiDocJson.info.group]; if (endpointsInfo?.length > 0) { endpointsInfo.forEach(endpointInfo => { - const sessionToken = this.userDataService.token || 'YOUR_SESSION_TOKEN'; const baseUrl = `${window.location.origin}${BASE_URL}/${this.serviceName}${endpointInfo.endpoint}`; - const headers = `-H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; + + let headers: string; + if (this.selectedApiKey) { + headers = `-H 'accept: application/json' -H '${API_KEY_HEADER}: ${this.selectedApiKey}'`; + } else { + const sessionToken = + this.userDataService.token || 'YOUR_SESSION_TOKEN'; + 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}`; From 822073e98d967d2c27df168cab2c85e42fac7cb5 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Wed, 30 Jul 2025 13:33:11 +0300 Subject: [PATCH 08/15] Used a prettier --- .../adf-api-docs/df-api-docs/df-api-docs.component.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 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 c839930a..e400b68d 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 @@ -20,8 +20,12 @@
- None (Session token based authentication) - Uses session token to build the request + None (Session token based authentication) + Uses session token to build the request
From 1db17c86523e4ad1e3d0610331b0254c1f8a3b94 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Wed, 30 Jul 2025 13:50:32 +0300 Subject: [PATCH 09/15] Remove duplicate --- src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts | 1 - 1 file changed, 1 deletion(-) 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 6c35c3d0..70bba90d 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 @@ -13,7 +13,6 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatIconModule } from '@angular/material/icon'; import { TranslocoModule } from '@ngneat/transloco'; -import { FormsModule } from '@angular/forms'; import { saveRawAsFile } from 'src/app/shared/utilities/file'; import { UntilDestroy } from '@ngneat/until-destroy'; import { DfUserDataService } from 'src/app/shared/services/df-user-data.service'; From 979886bc3a5a60358b5101c985e21beaa09a5771 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Wed, 30 Jul 2025 13:50:58 +0300 Subject: [PATCH 10/15] Prettier --- src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 70bba90d..1df93c6c 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 @@ -350,7 +350,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { }, }); } - + reloadApiDocs() { if (!this.serviceName) return; const params = this.expandSchema ? '?expand_schema=true' : ''; From 1bf864458621440a84fe7d06b559cd99eab413bd Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Wed, 30 Jul 2025 18:22:15 +0300 Subject: [PATCH 11/15] Initial implementation for df api tester component --- .../df-api-docs/df-api-docs.component.html | 7 +- .../df-api-docs/df-api-docs.component.ts | 15 +- .../df-api-quickstart.component.html | 4 + .../df-api-quickstart.component.scss | 4 + .../df-api-quickstart.component.ts | 85 ++--- .../df-api-tester.component.html | 183 +++++++++++ .../df-api-tester.component.scss | 203 ++++++++++++ .../df-api-tester/df-api-tester.component.ts | 305 ++++++++++++++++++ 8 files changed, 733 insertions(+), 73 deletions(-) create mode 100644 src/app/shared/components/df-api-tester/df-api-tester.component.html create mode 100644 src/app/shared/components/df-api-tester/df-api-tester.component.scss create mode 100644 src/app/shared/components/df-api-tester/df-api-tester.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 2c34e17c..07d3d1b2 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 @@ -15,9 +15,7 @@
{{ 'apiDocs.apiKeys.label' | transloco }} - +
@@ -94,8 +92,7 @@ + [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 1df93c6c..746ac77a 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 @@ -112,7 +112,6 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { apiDocJson: ApiDocJson; apiKeys: ApiKeyInfo[] = []; - selectedApiKey: string | null = null; faCopy = faCopy; expandSchema = false; @@ -296,23 +295,13 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.showUnhealthyErrorDetails = !this.showUnhealthyErrorDetails; } - onApiKeySelectionChange(selectedKey: string | null): void { - this.selectedApiKey = selectedKey; - // Regenerate Swagger documentation with the new API key (or null for session token) - this.generateSwaggerWithApiKey(this.apiDocJson); - } - private generateSwaggerWithApiKey(apiDocumentation: ApiDocJson): void { SwaggerUI({ spec: apiDocumentation, domNode: this.apiDocElement?.nativeElement, requestInterceptor: (req: SwaggerUI.Request) => { - if (this.selectedApiKey == null) { - req['headers'][SESSION_TOKEN_HEADER] = this.userDataService.token; - } - // Use selected API key if available, otherwise fall back to environment key - const apiKey = this.selectedApiKey || environment.dfApiDocsApiKey; - req['headers'][API_KEY_HEADER] = apiKey; + req['headers'][SESSION_TOKEN_HEADER] = this.userDataService.token; + req['headers'][API_KEY_HEADER] = environment.dfApiDocsApiKey; // Parse the request URL const url = new URL(req['url']); const params = new URLSearchParams(url.search); 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 index 0d73cff7..0d63af61 100644 --- 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 @@ -52,4 +52,8 @@

+ + + + 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 index d6e9a152..3b9057a6 100644 --- 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 @@ -60,3 +60,7 @@ mat-expansion-panel-header { background-color: #f93e3e; // red } } + +.themed-text { + color: var(--df-primary-text-color); +} diff --git a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts index a46da826..52db7454 100644 --- a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts @@ -11,11 +11,13 @@ import { faCopy } from '@fortawesome/free-solid-svg-icons'; import { Clipboard } from '@angular/cdk/clipboard'; 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 { + 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'; -import { API_KEY_HEADER } from 'src/app/shared/constants/http-headers'; +import { DfApiTesterComponent } from 'src/app/shared/components/df-api-tester/df-api-tester.component'; interface CurlCommand { title: string; @@ -25,32 +27,6 @@ interface CurlCommand { 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-api-quickstart', templateUrl: './df-api-quickstart.component.html', @@ -66,12 +42,12 @@ const healthCheckEndpointsInfo: { FontAwesomeModule, MatDividerModule, MatButtonModule, + DfApiTesterComponent, ], }) export class DfApiQuickstartComponent implements OnChanges { @Input() apiDocJson: ApiDocJson; @Input() serviceName: string; - @Input() selectedApiKey: string | null = null; curlCommands: CurlCommand[] = []; faCopy = faCopy; @@ -84,9 +60,7 @@ export class DfApiQuickstartComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if ( - (changes['apiDocJson'] || - changes['serviceName'] || - changes['selectedApiKey']) && + (changes['apiDocJson'] || changes['serviceName']) && this.apiDocJson && this.serviceName ) { @@ -100,36 +74,37 @@ export class DfApiQuickstartComponent implements OnChanges { private prepareCurlCommands(): void { this.curlCommands = []; - if (!this.serviceName || !this.apiDocJson?.info?.group) { + if (!this.serviceName || !this.apiDocJson?.paths) { return; } - const endpointsInfo = healthCheckEndpointsInfo[this.apiDocJson.info.group]; - if (endpointsInfo?.length > 0) { - endpointsInfo.forEach(endpointInfo => { - const baseUrl = `${window.location.origin}${BASE_URL}/${this.serviceName}${endpointInfo.endpoint}`; + // Generate cURL commands for the first few GET endpoints + const getEndpoints = Object.keys(this.apiDocJson.paths) + .filter(path => { + const pathData = this.apiDocJson.paths[path]; + return pathData['get'] && typeof pathData['get'] === 'object'; + }) + .slice(0, 3); // Limit to first 3 GET endpoints for quickstart - let headers: string; - if (this.selectedApiKey) { - headers = `-H 'accept: application/json' -H '${API_KEY_HEADER}: ${this.selectedApiKey}'`; - } else { - const sessionToken = - this.userDataService.token || 'YOUR_SESSION_TOKEN'; - headers = `-H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; - } + getEndpoints.forEach(endpoint => { + const operation = this.apiDocJson.paths[endpoint]['get']; + const baseUrl = `${window.location.origin}${BASE_URL}/${this.serviceName}${endpoint}`; - const commandForDisplay = `curl -X 'GET' '${baseUrl}' \\\n ${headers}`; - const commandForCopy = `curl -X 'GET' '${baseUrl}' ${headers}`; + let headers: string; + const sessionToken = this.userDataService.token || 'YOUR_SESSION_TOKEN'; + headers = `-H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; - this.curlCommands.push({ - title: endpointInfo.title, - description: endpointInfo.description, - textForDisplay: commandForDisplay, - textForCopy: commandForCopy, - note: this.apiDocJson.paths[endpointInfo.endpoint]?.['get']?.summary, - }); + const commandForDisplay = `curl -X 'GET' '${baseUrl}' \\\n ${headers}`; + const commandForCopy = `curl -X 'GET' '${baseUrl}' ${headers}`; + + this.curlCommands.push({ + title: operation?.summary || `GET ${endpoint}`, + description: operation?.description || `Retrieve data from ${endpoint}`, + textForDisplay: commandForDisplay, + textForCopy: commandForCopy, + note: operation?.summary || '', }); - } + }); } trackByCommand(index: number, item: CurlCommand): string { diff --git a/src/app/shared/components/df-api-tester/df-api-tester.component.html b/src/app/shared/components/df-api-tester/df-api-tester.component.html new file mode 100644 index 00000000..ffec42f2 --- /dev/null +++ b/src/app/shared/components/df-api-tester/df-api-tester.component.html @@ -0,0 +1,183 @@ + + + + Test API Authentication + + + Validate endpoint access with different authentication methods + + + +
+

+ Test your API endpoints to validate authentication and security + configurations. +

+ + +
+ + Select Endpoint + + +
+
+ + {{ endpoint.method }} + + {{ endpoint.endpoint }} +
+ {{ endpoint.title }} +
+
+
+
+ + + + Authentication Method + + +
+ Session Token + Use current session +
+
+ +
+ {{ key.name }} + {{ key.apiKey | slice: 0 : 8 }}... +
+
+
+
+ + + +
+ + + + +
+ + {{ getSelectedEndpoint()!.method }} + +

{{ getSelectedEndpoint()?.title }}

+
+

{{ getSelectedEndpoint()?.description }}

+
+ Endpoint: + {{ getSelectedEndpoint()?.endpoint }} + Method: + {{ getSelectedEndpoint()?.method }} + Authentication: + {{ getAuthenticationMethod() }} + Operation ID: + {{ getSelectedEndpoint()?.operationId }} +
+
+
+ + + + + + + Authentication Successful + Authentication Failed + Request Failed + + + + +
+ Status Code: {{ testResult.status }} + + Result: Access granted - authentication is + working correctly + + + Issue: {{ testResult.error }} + + + Error: {{ testResult.error }} + +
+ + +
+
Authentication Help:
+
    +
  • + Try selecting a different API key from the dropdown above +
  • +
  • + The selected API key may not have access to this endpoint +
  • +
  • + Check if the service has proper role-based access configured +
  • +
  • Verify the API key is active and not expired
  • +
+
+
+
+
+
+
diff --git a/src/app/shared/components/df-api-tester/df-api-tester.component.scss b/src/app/shared/components/df-api-tester/df-api-tester.component.scss new file mode 100644 index 00000000..688fb707 --- /dev/null +++ b/src/app/shared/components/df-api-tester/df-api-tester.component.scss @@ -0,0 +1,203 @@ +// API Tester Component Styles +.api-tester-container { + margin-top: 16px; +} + +.description-text { + color: var(--df-secondary-text-color); + margin-bottom: 16px; +} + +.test-controls { + display: grid; + grid-template-columns: 2fr 1fr auto; + gap: 16px; + align-items: end; + margin: 16px 0; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: 12px; + } +} + +.endpoint-select, +.api-key-select { + min-width: 150px; +} + +.endpoint-option, +.method-option, +.auth-option { + display: flex; + flex-direction: column; + gap: 4px; +} + +.endpoint-header { + display: flex; + align-items: center; + gap: 8px; +} + +.method-badge { + font-size: 10px; + font-weight: bold; + color: white; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; + min-width: 45px; + text-align: center; + + &.large { + font-size: 12px; + padding: 4px 8px; + min-width: 50px; + } +} + +.endpoint-path, +.method-name, +.auth-name { + font-weight: 500; + font-family: monospace; +} + +.endpoint-title, +.auth-desc { + font-size: 0.85em; + color: var(--df-secondary-text-color); +} + +.test-button { + height: 56px; + min-width: 120px; + + fa-icon, + mat-spinner { + margin-right: 8px; + } +} + +.endpoint-info-card { + margin: 16px 0; + background: var(--df-surface-color); + + .endpoint-info-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; + + h4 { + margin: 0; + color: var(--df-primary-text-color); + } + } + + p { + margin: 0 0 12px 0; + color: var(--df-secondary-text-color); + } + + .test-details { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 8px; + font-size: 0.9em; + + span { + color: var(--df-secondary-text-color); + + strong { + color: var(--df-primary-text-color); + } + } + } +} + +.test-result-card { + margin: 16px 0; + + &.success-result { + border-left: 4px solid #4caf50; + background: rgba(76, 175, 80, 0.05); + } + + &.error-result { + border-left: 4px solid #f44336; + background: rgba(244, 67, 54, 0.05); + } + + &.auth-error { + border-left: 4px solid #ff9800; + background: rgba(255, 152, 0, 0.05); + } + + mat-card-header { + display: flex; + align-items: center; + justify-content: space-between; + + mat-card-title { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + font-size: 1.1em; + } + + .clear-result-btn { + opacity: 0.7; + + &:hover { + opacity: 1; + } + } + } + + .result-summary { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; + + span { + font-size: 0.95em; + + strong { + color: var(--df-primary-text-color); + } + } + } + + .auth-guidance { + margin-top: 16px; + padding: 12px; + background: rgba(255, 152, 0, 0.1); + border-radius: 4px; + border-left: 3px solid #ff9800; + + h5 { + margin: 0 0 8px 0; + color: var(--df-primary-text-color); + font-size: 0.9em; + } + + ul { + margin: 0; + padding-left: 20px; + + li { + font-size: 0.85em; + color: var(--df-secondary-text-color); + margin-bottom: 4px; + + &:last-child { + margin-bottom: 0; + } + } + } + } +} diff --git a/src/app/shared/components/df-api-tester/df-api-tester.component.ts b/src/app/shared/components/df-api-tester/df-api-tester.component.ts new file mode 100644 index 00000000..99fe4b35 --- /dev/null +++ b/src/app/shared/components/df-api-tester/df-api-tester.component.ts @@ -0,0 +1,305 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { MatCardModule } from '@angular/material/card'; +import { MatSelectModule } from '@angular/material/select'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatButtonModule } from '@angular/material/button'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatIconModule } from '@angular/material/icon'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { FormsModule } from '@angular/forms'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faPlay, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons'; + +import { DfUserDataService } from 'src/app/shared/services/df-user-data.service'; +import { DfCurrentServiceService } from 'src/app/shared/services/df-current-service.service'; +import { ApiKeysService } from '../../../adf-api-docs/services/api-keys.service'; +import { ApiKeyInfo } from 'src/app/shared/types/api-keys'; +import { ApiDocJson } from 'src/app/shared/types/files'; +import { BASE_URL } from 'src/app/shared/constants/urls'; +import { + SESSION_TOKEN_HEADER, + API_KEY_HEADER, +} from 'src/app/shared/constants/http-headers'; + +interface TestEndpoint { + endpoint: string; + method: string; + title: string; + description: string; + operationId?: string; +} + +interface TestResult { + success: boolean; + status: number; + error?: string; +} + +@Component({ + selector: 'df-api-tester', + templateUrl: './df-api-tester.component.html', + styleUrls: ['./df-api-tester.component.scss'], + standalone: true, + imports: [ + CommonModule, + MatCardModule, + MatSelectModule, + MatFormFieldModule, + MatButtonModule, + MatProgressSpinnerModule, + MatExpansionModule, + MatIconModule, + FormsModule, + FontAwesomeModule, + ], +}) +export class DfApiTesterComponent implements OnChanges { + @Input() apiDocJson: ApiDocJson; + @Input() serviceName: string; + + // Icons + faPlay = faPlay; + faCheck = faCheck; + faTimes = faTimes; + + // API Testing properties + availableEndpoints: TestEndpoint[] = []; + selectedEndpointIndex: number = 0; + selectedApiKey: string | null = null; + availableApiKeys: ApiKeyInfo[] = []; + testResult: TestResult | null = null; + isTesting = false; + + constructor( + private http: HttpClient, + private userDataService: DfUserDataService, + private snackBar: MatSnackBar, + private apiKeysService: ApiKeysService, + private currentServiceService: DfCurrentServiceService + ) {} + + ngOnChanges(changes: SimpleChanges): void { + if ( + (changes['apiDocJson'] || changes['serviceName']) && + this.apiDocJson && + this.serviceName + ) { + this.prepareTestEndpoints(); + this.loadApiKeys(); + } + } + + private prepareTestEndpoints(): void { + this.availableEndpoints = []; + if (!this.serviceName || !this.apiDocJson?.paths) { + return; + } + + // Extract endpoints from apiDocJson.paths + Object.keys(this.apiDocJson.paths).forEach(path => { + const pathData = this.apiDocJson.paths[path]; + + // Get available HTTP methods for this path + const methods = ['get', 'post', 'put', 'patch', 'delete'].filter( + method => pathData[method] && typeof pathData[method] === 'object' + ); + + methods.forEach(method => { + const operation = pathData[method]; + if (operation && operation.summary) { + this.availableEndpoints.push({ + endpoint: path, + method: method.toUpperCase(), + title: operation.summary, + description: operation.description || operation.summary, + operationId: operation.operationId, + }); + } + }); + }); + + // Sort endpoints by path and method for better organization + this.availableEndpoints.sort((a, b) => { + if (a.endpoint !== b.endpoint) { + return a.endpoint.localeCompare(b.endpoint); + } + return a.method.localeCompare(b.method); + }); + + // Select first endpoint by default + if (this.availableEndpoints.length > 0) { + this.selectedEndpointIndex = 0; + } + } + + private loadApiKeys(): void { + // Get API keys from the current service + this.currentServiceService.getCurrentServiceId().subscribe({ + next: (serviceId: number) => { + this.apiKeysService.getApiKeysForService(serviceId).subscribe({ + next: (keys: ApiKeyInfo[]) => { + this.availableApiKeys = keys; + }, + error: (error: any) => { + console.error('Failed to load API keys:', error); + this.availableApiKeys = []; + }, + }); + }, + error: (error: any) => { + console.error('Failed to get service ID:', error); + this.availableApiKeys = []; + }, + }); + } + + testEndpoint(): void { + const selectedEndpoint = this.getSelectedEndpoint(); + if (!selectedEndpoint || !this.serviceName) { + this.snackBar.open('Please select an endpoint to test', 'Close', { + duration: 3000, + }); + return; + } + + this.isTesting = true; + this.testResult = null; + + const baseUrl = `${window.location.origin}${BASE_URL}/${this.serviceName}${selectedEndpoint.endpoint}`; + + let headers = new HttpHeaders({ + accept: 'application/json', + 'content-type': 'application/json', + }); + + if (this.selectedApiKey && this.selectedApiKey.trim()) { + // Use API key authentication + headers = headers.set(API_KEY_HEADER, this.selectedApiKey); + } else { + // Use session token authentication + const sessionToken = this.userDataService.token; + if (sessionToken) { + headers = headers.set(SESSION_TOKEN_HEADER, sessionToken); + } + } + + // Prepare request options + const requestOptions = { + headers, + observe: 'response' as const, + }; + + // Execute request based on selected endpoint's method + let request; + switch (selectedEndpoint.method.toLowerCase()) { + case 'get': + request = this.http.get(baseUrl, requestOptions); + break; + case 'post': + request = this.http.post(baseUrl, {}, requestOptions); + break; + case 'put': + request = this.http.put(baseUrl, {}, requestOptions); + break; + case 'patch': + request = this.http.patch(baseUrl, {}, requestOptions); + break; + case 'delete': + request = this.http.delete(baseUrl, requestOptions); + break; + default: + this.snackBar.open('Unsupported HTTP method', 'Close', { + duration: 3000, + }); + this.isTesting = false; + return; + } + + request.subscribe({ + next: response => { + this.testResult = { + success: true, + status: response.status, + }; + this.isTesting = false; + this.snackBar.open( + `${selectedEndpoint.method} request successful! Authentication verified.`, + 'Close', + { + duration: 3000, + } + ); + }, + error: error => { + const isAuthError = error.status === 401 || error.status === 403; + this.testResult = { + success: false, + status: error.status || 0, + error: isAuthError + ? 'Authentication failed' + : error.error?.error?.message || error.message || 'Unknown error', + }; + this.isTesting = false; + + if (isAuthError) { + this.snackBar.open( + 'Authentication failed! Check your API key or session.', + 'Close', + { + duration: 5000, + } + ); + } else { + this.snackBar.open( + `${selectedEndpoint.method} request failed. Check the results below.`, + 'Close', + { + duration: 3000, + } + ); + } + }, + }); + } + + clearTestResult(): void { + this.testResult = null; + } + + getSelectedEndpoint(): TestEndpoint | null { + return this.availableEndpoints[this.selectedEndpointIndex] || null; + } + + getAuthenticationMethod(): string { + return this.selectedApiKey ? 'API Key' : 'Session Token'; + } + + onEndpointChange(): void { + // Clear previous test results when endpoint changes + this.testResult = null; + } + + getMethodColor(method: string): string { + switch (method.toLowerCase()) { + case 'get': + return '#61affe'; + case 'post': + return '#49cc90'; + case 'put': + return '#fca130'; + case 'patch': + return '#50e3c2'; + case 'delete': + return '#f93e3e'; + default: + return '#9b9b9b'; + } + } + + isAuthenticationError(): boolean { + return this.testResult?.status === 401 || this.testResult?.status === 403; + } +} From 3674542efb567b4699c37d96782db8596ab6ea81 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Wed, 30 Jul 2025 18:22:41 +0300 Subject: [PATCH 12/15] Prittier --- .../df-api-quickstart/df-api-quickstart.component.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts index 52db7454..649c9228 100644 --- a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts @@ -11,9 +11,7 @@ import { faCopy } from '@fortawesome/free-solid-svg-icons'; import { Clipboard } from '@angular/cdk/clipboard'; 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 { 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'; From 2f94551b5f1a19fb2e46c904e62c36fe6f768178 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Thu, 31 Jul 2025 11:41:02 +0300 Subject: [PATCH 13/15] Reverting changes for available api keys select component, so it won't affect any component. Making some improvements for test api component --- .../constants/health-check-endpoints.ts | 25 +++++++++++ .../df-api-docs/df-api-docs.component.ts | 29 +------------ .../df-api-quickstart.component.ts | 43 ++++++++----------- .../df-api-tester.component.html | 38 ++++++++++------ .../df-api-tester.component.scss | 9 +++- .../df-api-tester/df-api-tester.component.ts | 24 ++++++++--- 6 files changed, 95 insertions(+), 73 deletions(-) create mode 100644 src/app/adf-api-docs/constants/health-check-endpoints.ts diff --git a/src/app/adf-api-docs/constants/health-check-endpoints.ts b/src/app/adf-api-docs/constants/health-check-endpoints.ts new file mode 100644 index 00000000..730dcc78 --- /dev/null +++ b/src/app/adf-api-docs/constants/health-check-endpoints.ts @@ -0,0 +1,25 @@ +export 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', + }, + ], +}; \ 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 746ac77a..0d10cc2b 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 @@ -55,6 +55,7 @@ import { BASE_URL } from 'src/app/shared/constants/urls'; import { Subscription, of, forkJoin } from 'rxjs'; import { DfApiQuickstartComponent } from '../df-api-quickstart/df-api-quickstart.component'; import { ApiDocJson } from 'src/app/shared/types/files'; +import { healthCheckEndpointsInfo } from '../constants/health-check-endpoints'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { FormsModule } from '@angular/forms'; @@ -120,32 +121,6 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { 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 - 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', - }, - ], - }; private rawHttp: HttpClient; @@ -228,7 +203,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { private checkApiHealth(): void { let endpointsInfoToValidate = - this.healthCheckEndpointsInfo[this.apiDocJson.info.group]; + healthCheckEndpointsInfo[this.apiDocJson.info.group]; if (this.serviceName && endpointsInfoToValidate) { // Perform health check this.performHealthCheck(endpointsInfoToValidate[0].endpoint); diff --git a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts index 649c9228..b0a3e241 100644 --- a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts @@ -16,6 +16,7 @@ import { ApiDocJson } from 'src/app/shared/types/files'; import { MatDividerModule } from '@angular/material/divider'; import { MatSnackBar } from '@angular/material/snack-bar'; import { DfApiTesterComponent } from 'src/app/shared/components/df-api-tester/df-api-tester.component'; +import { healthCheckEndpointsInfo } from '../constants/health-check-endpoints'; interface CurlCommand { title: string; @@ -72,37 +73,29 @@ export class DfApiQuickstartComponent implements OnChanges { private prepareCurlCommands(): void { this.curlCommands = []; - if (!this.serviceName || !this.apiDocJson?.paths) { + if (!this.serviceName || !this.apiDocJson?.info?.group) { return; } - // Generate cURL commands for the first few GET endpoints - const getEndpoints = Object.keys(this.apiDocJson.paths) - .filter(path => { - const pathData = this.apiDocJson.paths[path]; - return pathData['get'] && typeof pathData['get'] === 'object'; - }) - .slice(0, 3); // Limit to first 3 GET endpoints for quickstart + const endpointsInfo = healthCheckEndpointsInfo[this.apiDocJson.info.group]; + if (endpointsInfo?.length > 0) { + endpointsInfo.forEach(endpointInfo => { + const sessionToken = this.userDataService.token || 'YOUR_SESSION_TOKEN'; + const baseUrl = `${window.location.origin}${BASE_URL}/${this.serviceName}${endpointInfo.endpoint}`; + const headers = `-H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; - getEndpoints.forEach(endpoint => { - const operation = this.apiDocJson.paths[endpoint]['get']; - const baseUrl = `${window.location.origin}${BASE_URL}/${this.serviceName}${endpoint}`; + const commandForDisplay = `curl -X 'GET' '${baseUrl}' \\\n ${headers}`; + const commandForCopy = `curl -X 'GET' '${baseUrl}' ${headers}`; - let headers: string; - const sessionToken = this.userDataService.token || 'YOUR_SESSION_TOKEN'; - 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: operation?.summary || `GET ${endpoint}`, - description: operation?.description || `Retrieve data from ${endpoint}`, - textForDisplay: commandForDisplay, - textForCopy: commandForCopy, - note: operation?.summary || '', + this.curlCommands.push({ + title: endpointInfo.title, + description: endpointInfo.description, + textForDisplay: commandForDisplay, + textForCopy: commandForCopy, + note: this.apiDocJson.paths[endpointInfo.endpoint]?.['get']?.summary || '', + }); }); - }); + } } trackByCommand(index: number, item: CurlCommand): string { diff --git a/src/app/shared/components/df-api-tester/df-api-tester.component.html b/src/app/shared/components/df-api-tester/df-api-tester.component.html index ffec42f2..73bd0b0c 100644 --- a/src/app/shared/components/df-api-tester/df-api-tester.component.html +++ b/src/app/shared/components/df-api-tester/df-api-tester.component.html @@ -33,7 +33,7 @@ {{ endpoint.endpoint }}

- {{ endpoint.title }} + {{ endpoint.title }}
@@ -120,20 +120,19 @@

{{ getSelectedEndpoint()?.title }}

appearance="outlined" [class.success-result]="testResult.success" [class.error-result]="!testResult.success" - [class.auth-error]="isAuthenticationError()"> + [class.auth-error]="!testResult.success && isAuthenticationError()" + [class.non-auth-error]="!testResult.success && !isAuthenticationError()"> - Authentication Successful + [style.color]="getResultIconColor()"> + ✅ Authentication & Request Successful Authentication Failed🔒 Authentication Failed Request Failed✅ Authentication OK - Request Failed (Non-Auth Issue)

Status Code: {{ testResult.status }} - Result: Access granted - authentication is - working correctly + Result: ✅ Authentication verified and access granted successfully - Issue: {{ testResult.error }} + Authentication Result: 🔒 Access denied - {{ testResult.error }} - Error: {{ testResult.error }} + Authentication Result: ✅ Authentication passed, but request failed due to: {{ testResult.error }}
@@ -162,7 +160,7 @@

{{ getSelectedEndpoint()?.title }}

-
Authentication Help:
+
🔒 Authentication Help:
  • Try selecting a different API key from the dropdown above @@ -176,6 +174,20 @@
    Authentication Help:
  • Verify the API key is active and not expired
+ + +
+
✅ Authentication Status: Passed
+

Good news! Your authentication is working correctly. The request failed for other reasons:

+
    +
  • The endpoint might require specific parameters or request body
  • +
  • The service might be temporarily unavailable
  • +
  • The endpoint might have validation rules that weren't met
  • +
  • Check the error message above for specific details
  • +
+
diff --git a/src/app/shared/components/df-api-tester/df-api-tester.component.scss b/src/app/shared/components/df-api-tester/df-api-tester.component.scss index 688fb707..d540429c 100644 --- a/src/app/shared/components/df-api-tester/df-api-tester.component.scss +++ b/src/app/shared/components/df-api-tester/df-api-tester.component.scss @@ -14,7 +14,9 @@ gap: 16px; align-items: end; margin: 16px 0; - + justify-content: center; + align-items: baseline; + @media (max-width: 768px) { grid-template-columns: 1fr; gap: 12px; @@ -131,6 +133,11 @@ } &.auth-error { + border-left: 4px solid #f44336; + background: rgba(244, 67, 54, 0.05); + } + + &.non-auth-error { border-left: 4px solid #ff9800; background: rgba(255, 152, 0, 0.05); } diff --git a/src/app/shared/components/df-api-tester/df-api-tester.component.ts b/src/app/shared/components/df-api-tester/df-api-tester.component.ts index 99fe4b35..90336597 100644 --- a/src/app/shared/components/df-api-tester/df-api-tester.component.ts +++ b/src/app/shared/components/df-api-tester/df-api-tester.component.ts @@ -226,10 +226,10 @@ export class DfApiTesterComponent implements OnChanges { }; this.isTesting = false; this.snackBar.open( - `${selectedEndpoint.method} request successful! Authentication verified.`, + `✅ Authentication successful! Access granted to ${selectedEndpoint.method} ${selectedEndpoint.endpoint}`, 'Close', { - duration: 3000, + duration: 4000, } ); }, @@ -239,14 +239,14 @@ export class DfApiTesterComponent implements OnChanges { success: false, status: error.status || 0, error: isAuthError - ? 'Authentication failed' - : error.error?.error?.message || error.message || 'Unknown error', + ? 'Authentication failed - Access denied' + : error.error?.error?.message || error.message || 'Request failed due to non-authentication error', }; this.isTesting = false; if (isAuthError) { this.snackBar.open( - 'Authentication failed! Check your API key or session.', + '🔒 Authentication failed! Your credentials do not have access to this endpoint.', 'Close', { duration: 5000, @@ -254,10 +254,10 @@ export class DfApiTesterComponent implements OnChanges { ); } else { this.snackBar.open( - `${selectedEndpoint.method} request failed. Check the results below.`, + `✅ Authentication successful, but request failed due to other reasons (Status: ${error.status}).`, 'Close', { - duration: 3000, + duration: 4000, } ); } @@ -302,4 +302,14 @@ export class DfApiTesterComponent implements OnChanges { isAuthenticationError(): boolean { return this.testResult?.status === 401 || this.testResult?.status === 403; } + + getResultIconColor(): string { + if (this.testResult?.success) { + return '#4caf50'; // Green for success + } else if (this.isAuthenticationError()) { + return '#f44336'; // Red for auth failure + } else { + return '#ff9800'; // Orange for non-auth failure (auth passed but request failed) + } + } } From b7827abc958e235a13aaebe398594c4d5914fff2 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Thu, 31 Jul 2025 11:55:55 +0300 Subject: [PATCH 14/15] Reverting changes for available api keys select component, so it won't affect any component. Making some improvements for test api component --- .../constants/health-check-endpoints.ts | 2 +- .../df-api-quickstart.component.ts | 4 ++- .../df-api-tester.component.html | 26 ++++++++++++++----- .../df-api-tester.component.scss | 4 +-- .../df-api-tester/df-api-tester.component.ts | 4 ++- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/app/adf-api-docs/constants/health-check-endpoints.ts b/src/app/adf-api-docs/constants/health-check-endpoints.ts index 730dcc78..1d0b737c 100644 --- a/src/app/adf-api-docs/constants/health-check-endpoints.ts +++ b/src/app/adf-api-docs/constants/health-check-endpoints.ts @@ -22,4 +22,4 @@ export const healthCheckEndpointsInfo: { 'This command fetches a list of folders from your connected file storage', }, ], -}; \ No newline at end of file +}; diff --git a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts index b0a3e241..44004533 100644 --- a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts @@ -92,7 +92,9 @@ export class DfApiQuickstartComponent implements OnChanges { description: endpointInfo.description, textForDisplay: commandForDisplay, textForCopy: commandForCopy, - note: this.apiDocJson.paths[endpointInfo.endpoint]?.['get']?.summary || '', + note: + this.apiDocJson.paths[endpointInfo.endpoint]?.['get']?.summary || + '', }); }); } diff --git a/src/app/shared/components/df-api-tester/df-api-tester.component.html b/src/app/shared/components/df-api-tester/df-api-tester.component.html index 73bd0b0c..a2da995e 100644 --- a/src/app/shared/components/df-api-tester/df-api-tester.component.html +++ b/src/app/shared/components/df-api-tester/df-api-tester.component.html @@ -121,13 +121,17 @@

{{ getSelectedEndpoint()?.title }}

[class.success-result]="testResult.success" [class.error-result]="!testResult.success" [class.auth-error]="!testResult.success && isAuthenticationError()" - [class.non-auth-error]="!testResult.success && !isAuthenticationError()"> + [class.non-auth-error]=" + !testResult.success && !isAuthenticationError() + "> - ✅ Authentication & Request Successful + ✅ Authentication & Request Successful 🔒 Authentication Failed @@ -146,13 +150,16 @@

{{ getSelectedEndpoint()?.title }}

Status Code: {{ testResult.status }} - Result: ✅ Authentication verified and access granted successfully + Result: ✅ Authentication verified and access + granted successfully - Authentication Result: 🔒 Access denied - {{ testResult.error }} + Authentication Result: 🔒 Access denied - + {{ testResult.error }} - Authentication Result: ✅ Authentication passed, but request failed due to: {{ testResult.error }} + Authentication Result: ✅ Authentication passed, + but request failed due to: {{ testResult.error }}
@@ -180,9 +187,14 @@
🔒 Authentication Help:
*ngIf="!testResult.success && !isAuthenticationError()" class="auth-guidance">
✅ Authentication Status: Passed
-

Good news! Your authentication is working correctly. The request failed for other reasons:

+

+ Good news! Your authentication is working + correctly. The request failed for other reasons: +

    -
  • The endpoint might require specific parameters or request body
  • +
  • + The endpoint might require specific parameters or request body +
  • The service might be temporarily unavailable
  • The endpoint might have validation rules that weren't met
  • Check the error message above for specific details
  • diff --git a/src/app/shared/components/df-api-tester/df-api-tester.component.scss b/src/app/shared/components/df-api-tester/df-api-tester.component.scss index d540429c..5fded544 100644 --- a/src/app/shared/components/df-api-tester/df-api-tester.component.scss +++ b/src/app/shared/components/df-api-tester/df-api-tester.component.scss @@ -14,9 +14,9 @@ gap: 16px; align-items: end; margin: 16px 0; - justify-content: center; + justify-content: center; align-items: baseline; - + @media (max-width: 768px) { grid-template-columns: 1fr; gap: 12px; diff --git a/src/app/shared/components/df-api-tester/df-api-tester.component.ts b/src/app/shared/components/df-api-tester/df-api-tester.component.ts index 90336597..1ea45159 100644 --- a/src/app/shared/components/df-api-tester/df-api-tester.component.ts +++ b/src/app/shared/components/df-api-tester/df-api-tester.component.ts @@ -240,7 +240,9 @@ export class DfApiTesterComponent implements OnChanges { status: error.status || 0, error: isAuthError ? 'Authentication failed - Access denied' - : error.error?.error?.message || error.message || 'Request failed due to non-authentication error', + : error.error?.error?.message || + error.message || + 'Request failed due to non-authentication error', }; this.isTesting = false; From 9867bd05ccabc86727e3ed8c08867eab6d8483b6 Mon Sep 17 00:00:00 2001 From: VitaliyHrabovych Date: Thu, 31 Jul 2025 15:13:15 +0300 Subject: [PATCH 15/15] Adjusting padding for accordion component --- .../components/df-api-tester/df-api-tester.component.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/shared/components/df-api-tester/df-api-tester.component.scss b/src/app/shared/components/df-api-tester/df-api-tester.component.scss index 5fded544..e1df8074 100644 --- a/src/app/shared/components/df-api-tester/df-api-tester.component.scss +++ b/src/app/shared/components/df-api-tester/df-api-tester.component.scss @@ -1,4 +1,7 @@ // API Tester Component Styles +mat-expansion-panel-header { + padding: 0 12px; +} .api-tester-container { margin-top: 16px; }