diff --git a/.gitignore b/.gitignore index 7a4a8630..58f8ab12 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ testem.log # System files .DS_Store Thumbs.db + +.config/ diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html index da274cff..9003c18f 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.html @@ -32,4 +32,54 @@ +
+
+
+

{{ 'apiHealthBanner.loading' | transloco }}

+
+
+

{{ 'apiHealthBanner.healthy' | transloco }}

+
+
+

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

+
+
{{ healthError }}
+
+
+
+

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

+
+
+ + +
+
diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss index 45708b99..f2cbf028 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.scss @@ -41,3 +41,106 @@ .swagger-ui { margin-top: 16px; } + +.api-health-banner { + display: flex; + align-items: center; + padding: 8px 12px; + border-radius: 4px; + border-left-width: 4px; + border-left-style: solid; + + p { + margin: 0; + font-size: 0.9em; + } + + &.status-healthy { + border-left-color: #28a745; + background-color: #e9f5ec; + color: #155724; + } + + &.status-unhealthy, + &.status-error { + border-left-color: #dc3545; + background-color: #f8d7da; + color: #721c24; + } + + &.status-unhealthy { + & > div { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + + & > p { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + + .view-details-button { + margin-left: 12px; + flex-shrink: 0; + padding: 2px 8px; + line-height: normal; + font-size: 0.9em; + min-width: auto; + } + } + + .unhealthy-error-details { + margin-top: 0; + padding: 8px 12px; + background-color: rgba(0, 0, 0, 0.03); + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 4px; + width: 100%; + box-sizing: border-box; + max-height: 150px; + overflow-y: auto; + + pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-size: 0.85em; + color: inherit; + } + } + } + } + + &.status-loading { + border-left-color: #007bff; + background-color: #e7f3ff; + color: #004085; + } + + &.status-warning { + border-left-color: #ffc107; + background-color: #fff3cd; + color: #856404; + } +} + +// Styles for elements within Swagger UI, piercing encapsulation +:host ::ng-deep { + .swagger-ui { + // This targets the wrapper div for Swagger UI in your component's template + .information-container { + .main { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; + } + } + } +} + +.custom-swagger-content-wrapper { + width: 100%; +} diff --git a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts index 8d805e37..26a293fa 100644 --- a/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts +++ b/src/app/adf-api-docs/df-api-docs/df-api-docs.component.ts @@ -25,19 +25,31 @@ import { mapSnakeToCamel, } from 'src/app/shared/utilities/case'; import { DfThemeService } from 'src/app/shared/services/df-theme.service'; -import { AsyncPipe, NgIf, NgFor, SlicePipe } from '@angular/common'; +import { AsyncPipe, NgIf, NgFor, SlicePipe, NgClass } from '@angular/common'; import { environment } from '../../../../environments/environment'; import { ApiKeysService } from '../services/api-keys.service'; import { ApiKeyInfo } from 'src/app/shared/types/api-keys'; import { Clipboard } from '@angular/cdk/clipboard'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatListModule } from '@angular/material/list'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatCardModule } from '@angular/material/card'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faCopy } from '@fortawesome/free-solid-svg-icons'; import { DfCurrentServiceService } from 'src/app/shared/services/df-current-service.service'; -import { tap, switchMap, map, distinctUntilChanged } from 'rxjs/operators'; -import { HttpClient } from '@angular/common/http'; +import { + tap, + switchMap, + map, + distinctUntilChanged, + catchError, +} from 'rxjs/operators'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { BASE_URL } from 'src/app/shared/constants/urls'; -import { Subscription } from 'rxjs'; +import { Subscription, of, forkJoin } from 'rxjs'; +import { DfApiQuickstartComponent } from '../df-api-quickstart/df-api-quickstart.component'; +import { ApiDocJson } from 'src/app/shared/types/files'; interface ServiceResponse { resource: Array<{ @@ -47,6 +59,12 @@ interface ServiceResponse { }>; } +interface HealthCheckResult { + endpoint: string; + success?: boolean; + error?: string; +} + @UntilDestroy({ checkProperties: true }) @Component({ selector: 'df-api-docs', @@ -63,18 +81,60 @@ interface ServiceResponse { NgIf, NgFor, SlicePipe, + NgClass, FontAwesomeModule, + MatListModule, + MatTooltipModule, + MatExpansionModule, + MatCardModule, + DfApiQuickstartComponent, ], }) export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { @ViewChild('apiDocumentation', { static: true }) apiDocElement: | ElementRef | undefined; + @ViewChild('swaggerInjectedContentContainer') + swaggerInjectedContentContainerRef: ElementRef | undefined; + @ViewChild('healthBannerElement') healthBannerElementRef: + | ElementRef + | undefined; - apiDocJson: object; + apiDocJson: ApiDocJson; apiKeys: ApiKeyInfo[] = []; faCopy = faCopy; + private subscriptions: Subscription[] = []; + healthStatus: 'loading' | 'healthy' | 'unhealthy' | 'warning' = 'loading'; + healthError: string | null = null; + serviceName: string | null = null; + showUnhealthyErrorDetails = false; + // Mapping of service types to their corresponding endpoints, probably would be better to move to the back-end + healthCheckEndpointsInfo: { + [key: string]: { endpoint: string; title: string; description: string }[]; + } = { + Database: [ + { + endpoint: '/_schema', + title: 'View Available Schemas', + description: + 'This command fetches a list of schemas from your connected database', + }, + { + endpoint: '/_table', + title: 'View Tables in Your Database', + description: 'This command lists all tables in your database', + }, + ], + File: [ + { + endpoint: '/', + title: 'View Available Folders', + description: + 'This command fetches a list of folders from your connected file storage', + }, + ], + }; constructor( private activatedRoute: ActivatedRoute, @@ -90,14 +150,14 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { isDarkMode = this.themeService.darkMode$; ngOnInit(): void { // Get the service name from the route - const serviceName = this.activatedRoute.snapshot.params['name']; + this.serviceName = this.activatedRoute.snapshot.params['name']; // First fetch the service ID by name - if (serviceName) { + if (this.serviceName) { this.subscriptions.push( this.http .get( - `${BASE_URL}/system/service?filter=name=${serviceName}` + `${BASE_URL}/system/service?filter=name=${this.serviceName}` ) .pipe( map(response => response?.resource?.[0]?.id || -1), @@ -115,11 +175,7 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.subscriptions.push( this.activatedRoute.data.subscribe(({ data }) => { if (data) { - if ( - data.paths['/']?.get && - data.paths['/']?.get.operationId && - data.paths['/']?.get.operationId === 'getSoapResources' - ) { + if (data.paths['/']?.get?.operationId === 'getSoapResources') { this.apiDocJson = { ...data, paths: mapSnakeToCamel(data.paths) }; } else { this.apiDocJson = { ...data, paths: mapCamelToSnake(data.paths) }; @@ -146,6 +202,8 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { ngAfterContentInit(): void { const apiDocumentation = this.apiDocJson; + this.checkApiHealth(); + SwaggerUI({ spec: apiDocumentation, domNode: this.apiDocElement?.nativeElement, @@ -165,6 +223,28 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { 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 + ); + } + }, }); } @@ -173,6 +253,51 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { this.subscriptions.forEach(sub => sub.unsubscribe()); } + private checkApiHealth(): void { + let endpointsInfoToValidate = + this.healthCheckEndpointsInfo[this.apiDocJson.info.group]; + if (this.serviceName && endpointsInfoToValidate) { + // Perform health check + this.performHealthCheck(endpointsInfoToValidate[0].endpoint); + } else { + this.setHealthState('warning'); + } + } + + private setHealthState( + status: 'healthy' | 'unhealthy' | 'warning', + error: string | null = null + ): void { + this.healthStatus = status; + this.healthError = error; + } + + private performHealthCheck(endpoint: string): void { + this.healthStatus = 'loading'; + this.healthError = null; + + this.subscriptions.push( + this.http + .get(`${BASE_URL}/${this.serviceName}${endpoint}`, { + responseType: 'text', + }) + .pipe( + tap(() => this.setHealthState('healthy')), + catchError((error: HttpErrorResponse) => { + this.setHealthState( + 'unhealthy', + `${endpoint}: ${ + error.message || error.error.message || 'Unknown error' + }` + ); + + return of(null); + }) + ) + .subscribe() + ); + } + goBackToList(): void { this.currentServiceService.clearCurrentServiceId(); this.router.navigate(['../'], { relativeTo: this.activatedRoute }); @@ -188,8 +313,31 @@ export class DfApiDocsComponent implements OnInit, AfterContentInit, OnDestroy { copyApiKey(key: string) { this.clipboard.copy(key); - this.snackBar.open('API Key copied to clipboard', 'Close', { - duration: 3000, + this.snackBar.open('API Key copied to clipboard!', 'Close', { + duration: 2000, }); } + + toggleUnhealthyErrorDetails(): void { + this.showUnhealthyErrorDetails = !this.showUnhealthyErrorDetails; + } + + private injectCustomContent( + swaggerContainer: HTMLElement, + infoContainer: HTMLElement | null, + customContentNode: HTMLElement + ): void { + if (infoContainer) { + infoContainer.appendChild(customContentNode); + } else { + if (swaggerContainer.firstChild) { + swaggerContainer.insertBefore( + customContentNode, + swaggerContainer.firstChild + ); + } else { + swaggerContainer.appendChild(customContentNode); + } + } + } } diff --git a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.html b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.html new file mode 100644 index 00000000..0d73cff7 --- /dev/null +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.html @@ -0,0 +1,55 @@ + + + + + {{ 'apiBasicCurlCommands.title' | transloco }} + + + +
+

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

+
+

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

+

+ {{ command.description }} +

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

{{ command.note }}

+
+ +
+

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

+
+
+
diff --git a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.scss b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.scss new file mode 100644 index 00000000..d6e9a152 --- /dev/null +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.scss @@ -0,0 +1,62 @@ +mat-expansion-panel-header { + padding: 0 12px; +} +.curl-command-text { + white-space: pre; + font-family: monospace; + font-size: 0.9em; + margin: 0; + color: var(--df-script-editor-text-color); + overflow-x: auto; +} + +.curl-commands-container { + display: flex; + flex-direction: column; + gap: 8px; + .actions-container { + padding: 0px 8px; + } +} + +.curl-command-title { + margin: 0; + font-weight: bold; +} + +.curl-command-note { + color: gray !important; +} + +.no-commands-container { + ul { + padding-left: 20px; + li { + margin-bottom: 10px; + } + } + + span[class^='method-'] { + font-weight: bold; + font-family: monospace; + padding: 2px 6px; + border-radius: 4px; + color: white; + } + + .method-get { + background-color: #61affe; // blue + } + .method-post { + background-color: #49cc90; // green + } + .method-put { + background-color: #fca130; // orange + } + .method-patch { + background-color: #fca130; // orange + } + .method-delete { + background-color: #f93e3e; // red + } +} diff --git a/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts new file mode 100644 index 00000000..6a97d20e --- /dev/null +++ b/src/app/adf-api-docs/df-api-quickstart/df-api-quickstart.component.ts @@ -0,0 +1,127 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslocoModule } from '@ngneat/transloco'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatButtonModule } from '@angular/material/button'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { faCopy } from '@fortawesome/free-solid-svg-icons'; +import { Clipboard } from '@angular/cdk/clipboard'; +import { DfUserDataService } from 'src/app/shared/services/df-user-data.service'; +import { BASE_URL } from 'src/app/shared/constants/urls'; +import { SESSION_TOKEN_HEADER } from 'src/app/shared/constants/http-headers'; +import { ApiDocJson } from 'src/app/shared/types/files'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatSnackBar } from '@angular/material/snack-bar'; + +interface CurlCommand { + title: string; + description: string; + textForDisplay: string; + textForCopy: string; + note: string; +} + +const healthCheckEndpointsInfo: { + [key: string]: { endpoint: string; title: string; description: string }[]; +} = { + Database: [ + { + endpoint: '/_schema', + title: 'View Available Schemas', + description: + 'This command fetches a list of schemas from your connected database', + }, + { + endpoint: '/_table', + title: 'View Tables in Your Database', + description: 'This command lists all tables in your database', + }, + ], + File: [ + { + endpoint: '/', + title: 'View Available Folders', + description: + 'This command fetches a list of folders from your connected file storage', + }, + ], +}; + +@Component({ + selector: 'df-api-quickstart', + templateUrl: './df-api-quickstart.component.html', + styleUrls: ['./df-api-quickstart.component.scss'], + standalone: true, + imports: [ + CommonModule, + TranslocoModule, + MatExpansionModule, + MatCardModule, + MatIconModule, + MatTooltipModule, + FontAwesomeModule, + MatDividerModule, + MatButtonModule, + ], +}) +export class DfApiQuickstartComponent implements OnChanges { + @Input() apiDocJson: ApiDocJson; + @Input() serviceName: string; + + curlCommands: CurlCommand[] = []; + faCopy = faCopy; + + constructor( + private clipboard: Clipboard, + private userDataService: DfUserDataService, + private snackBar: MatSnackBar + ) {} + + ngOnChanges(changes: SimpleChanges): void { + if ( + (changes['apiDocJson'] || changes['serviceName']) && + this.apiDocJson && + this.serviceName + ) { + this.prepareCurlCommands(); + } + } + + copyCurlCommand(commandText: string) { + this.clipboard.copy(commandText); + } + + private prepareCurlCommands(): void { + this.curlCommands = []; + if (!this.serviceName || !this.apiDocJson?.info?.group) { + return; + } + + const endpointsInfo = healthCheckEndpointsInfo[this.apiDocJson.info.group]; + if (endpointsInfo?.length > 0) { + endpointsInfo.forEach(endpointInfo => { + const sessionToken = this.userDataService.token || 'YOUR_SESSION_TOKEN'; + const baseUrl = `${window.location.origin}${BASE_URL}/${this.serviceName}${endpointInfo.endpoint}`; + const headers = `-H 'accept: application/json' -H '${SESSION_TOKEN_HEADER}: ${sessionToken}'`; + + const commandForDisplay = `curl -X 'GET' '${baseUrl}' \\\n ${headers}`; + const commandForCopy = `curl -X 'GET' '${baseUrl}' ${headers}`; + + this.curlCommands.push({ + title: endpointInfo.title, + description: endpointInfo.description, + textForDisplay: commandForDisplay, + textForCopy: commandForCopy, + note: this.apiDocJson.paths[endpointInfo.endpoint]?.['get']?.summary, + }); + }); + } + } + + trackByCommand(index: number, item: CurlCommand): string { + return item.textForCopy; + } +} diff --git a/src/app/adf-event-scripts/df-manage-scripts/df-manage-scripts.component.html b/src/app/adf-event-scripts/df-manage-scripts/df-manage-scripts.component.html index 9da55379..d6a6c1d8 100644 --- a/src/app/adf-event-scripts/df-manage-scripts/df-manage-scripts.component.html +++ b/src/app/adf-event-scripts/df-manage-scripts/df-manage-scripts.component.html @@ -1,4 +1,6 @@ - + diff --git a/src/app/adf-home/df-icon-link/df-icon-link.component.scss b/src/app/adf-home/df-icon-link/df-icon-link.component.scss index e5c3f06c..656fc2ab 100644 --- a/src/app/adf-home/df-icon-link/df-icon-link.component.scss +++ b/src/app/adf-home/df-icon-link/df-icon-link.component.scss @@ -5,7 +5,7 @@ $df-purple-palette: mat.define-palette(theme.$df-purple-palette); .plain-icon-link { color: mat.get-color-from-palette($df-purple-palette, 600); display: flex; - justify-content: flex-start; + justify-content: left; align-items: center; text-decoration: none; padding: 5px 0px; diff --git a/src/app/adf-home/df-welcome-page/df-generate-api-card/df-generate-api-card.component.html b/src/app/adf-home/df-welcome-page/df-generate-api-card/df-generate-api-card.component.html new file mode 100644 index 00000000..161d4cd6 --- /dev/null +++ b/src/app/adf-home/df-welcome-page/df-generate-api-card/df-generate-api-card.component.html @@ -0,0 +1,16 @@ + + + +

+ {{ headerText | transloco }} +

+ {{ text | transloco }} +
+
diff --git a/src/app/adf-home/df-welcome-page/df-generate-api-card/df-generate-api-card.component.scss b/src/app/adf-home/df-welcome-page/df-generate-api-card/df-generate-api-card.component.scss new file mode 100644 index 00000000..f8f51e9f --- /dev/null +++ b/src/app/adf-home/df-welcome-page/df-generate-api-card/df-generate-api-card.component.scss @@ -0,0 +1,38 @@ +.df-generate-api-card { + cursor: pointer; + width: 100%; + min-height: 115px; + display: flex; + flex-direction: column; +} + +.df-generate-api-card-content { + display: flex; + flex-direction: column; + align-items: left; + padding: 12px; + flex-grow: 1; + overflow: hidden; + + fa-icon { + margin-bottom: 8px; + flex-shrink: 0; + } + + .df-card-header { + font-size: 14px; + font-weight: 500; + margin-bottom: 0px; + line-height: 1.2; + } + .df-card-description { + font-size: 12px; + line-height: 1.4; + display: -webkit-box; + line-clamp: 2; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/src/app/adf-home/df-welcome-page/df-generate-api-card/df-generate-api-card.component.ts b/src/app/adf-home/df-welcome-page/df-generate-api-card/df-generate-api-card.component.ts new file mode 100644 index 00000000..3db3dfb3 --- /dev/null +++ b/src/app/adf-home/df-welcome-page/df-generate-api-card/df-generate-api-card.component.ts @@ -0,0 +1,22 @@ +import { Component, Input } from '@angular/core'; +import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; +import { RouterModule } from '@angular/router'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TranslocoPipe } from '@ngneat/transloco'; +import { MatCardModule } from '@angular/material/card'; + +@Component({ + selector: 'df-generate-api-card', + templateUrl: './df-generate-api-card.component.html', + styleUrls: ['./df-generate-api-card.component.scss'], + standalone: true, + imports: [RouterModule, FontAwesomeModule, TranslocoPipe, MatCardModule], +}) +export class DfGenerateApiCardComponent { + @Input() icon: IconDefinition; + @Input() headerText: string; + @Input() text: string; + @Input() routerLink: string; + @Input() cardFinalBackgroundColor: string; + @Input() cardFinalHeaderColor: string; +} diff --git a/src/app/adf-home/df-welcome-page/df-welcome-page.component.html b/src/app/adf-home/df-welcome-page/df-welcome-page.component.html index b70f3c3c..c0edaf1b 100644 --- a/src/app/adf-home/df-welcome-page/df-welcome-page.component.html +++ b/src/app/adf-home/df-welcome-page/df-welcome-page.component.html @@ -9,27 +9,40 @@

- {{ 'home.welcomePage.welcomeHeading' | transloco }} + {{ 'home.welcomePage.generateYourApis' | transloco }}

-
+ + +
+

+ {{ 'home.welcomePage.needHelpChoosing' | transloco }}

-
    -
  • - +

    + {{ 'home.welcomePage.learnMoreHeading' | transloco }} +

    +
diff --git a/src/app/adf-home/df-welcome-page/df-welcome-page.component.scss b/src/app/adf-home/df-welcome-page/df-welcome-page.component.scss index 82279447..f28920c5 100644 --- a/src/app/adf-home/df-welcome-page/df-welcome-page.component.scss +++ b/src/app/adf-home/df-welcome-page/df-welcome-page.component.scss @@ -29,6 +29,7 @@ $df-coral-palette: mat.define-palette(theme.$df-coral-palette); &:first-child { padding-right: 25px; } + &:last-child { padding-left: 25px; } @@ -69,6 +70,7 @@ $df-coral-palette: mat.define-palette(theme.$df-coral-palette); mat-card.notice-card { padding: 30px; margin-bottom: 40px; + p { a { color: mat.get-color-from-palette($df-purple-palette, 600); @@ -86,6 +88,7 @@ mat-card.notice-card { p { color: white; } + .notice-card { background-color: mat.get-color-from-palette( $df-purple-palette, @@ -93,13 +96,57 @@ mat-card.notice-card { ) !important; } } + .release-container { display: flex; gap: 15px; } + .release-card { background-color: mat.get-color-from-palette($df-purple-palette, 100); } + .release-btn { margin-left: 10px; } + +.button-group { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: space-between; + margin-bottom: 16px; + + df-generate-api-card { + width: 32%; + + @media (min-width: 960px) and (max-width: 1480px) { + width: 48%; + } + + @media (max-width: 600px) { + width: 48%; + } + + @media (max-width: 350px) { + width: 98%; + } + } +} + +#need-help-choosing-note, +#learn-more-heading { + text-align: center; + margin: 0; +} + +#need-help-choosing-note { + font-size: 12px; +} + +.learn-more-links-list { + display: flex; + justify-content: space-evenly; + flex-wrap: wrap; + gap: 10px; +} diff --git a/src/app/adf-home/df-welcome-page/df-welcome-page.component.ts b/src/app/adf-home/df-welcome-page/df-welcome-page.component.ts index b0db10c3..a5d6ff9b 100644 --- a/src/app/adf-home/df-welcome-page/df-welcome-page.component.ts +++ b/src/app/adf-home/df-welcome-page/df-welcome-page.component.ts @@ -1,5 +1,16 @@ import { Component, Inject } from '@angular/core'; import { CommonModule } from '@angular/common'; // Add this import +import { RouterModule } from '@angular/router'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { + faDatabase, + faCode, + faNetworkWired, + faFile, + faTools, + faBook, +} from '@fortawesome/free-solid-svg-icons'; import { javaScriptExampleLinks, @@ -27,6 +38,9 @@ import { DfThemeService } from 'src/app/shared/services/df-theme.service'; import { DfBaseCrudService } from 'src/app/shared/services/df-base-crud.service'; import { SERVICES_SERVICE_TOKEN } from 'src/app/shared/constants/tokens'; import { DFStorageService } from 'src/app/shared/services/df-storage.service'; +import { ROUTES } from '../../shared/types/routes'; +import { DfGenerateApiCardComponent } from './df-generate-api-card/df-generate-api-card.component'; + @Component({ selector: 'df-welcome-page', templateUrl: './df-welcome-page.component.html', @@ -45,6 +59,10 @@ import { DFStorageService } from 'src/app/shared/services/df-storage.service'; DfQuickstartPageComponent, DfResourcesPageComponent, DfDownloadPageComponent, + RouterModule, + MatButtonModule, + MatIconModule, + DfGenerateApiCardComponent, ], providers: [DfBaseCrudService], }) @@ -52,6 +70,21 @@ export class DfWelcomePageComponent { faCirclePlay = faCirclePlay; faHeart = faHeart; faComment = faComment; + faDatabase = faDatabase; + faCode = faCode; + faNetworkWired = faNetworkWired; + faFile = faFile; + faTools = faTools; + faBook = faBook; + + fpDBRoute = `/${ROUTES.API_CONNECTIONS}/${ROUTES.API_TYPES}/${ROUTES.DATABASE}/${ROUTES.CREATE}`; + fpScriptingRoute = `/${ROUTES.API_CONNECTIONS}/${ROUTES.API_TYPES}/${ROUTES.SCRIPTING}/${ROUTES.CREATE}`; + fpNetworkRoute = `/${ROUTES.API_CONNECTIONS}/${ROUTES.API_TYPES}/${ROUTES.NETWORK}/${ROUTES.CREATE}`; + fpFileRoute = `/${ROUTES.API_CONNECTIONS}/${ROUTES.API_TYPES}/${ROUTES.FILE}/${ROUTES.CREATE}`; + fpUtilityRoute = `/${ROUTES.API_CONNECTIONS}/${ROUTES.API_TYPES}/${ROUTES.UTILITY}/${ROUTES.CREATE}`; + fpApiDocsRoute = `/${ROUTES.API_CONNECTIONS}/${ROUTES.API_DOCS}`; + + public generateApiCardsData: any[]; welcomePageResources = welcomePageResources; nativeExampleLinks = nativeExampleLinks; @@ -62,7 +95,58 @@ export class DfWelcomePageComponent { private themeService: DfThemeService, private storageService: DFStorageService, @Inject(SERVICES_SERVICE_TOKEN) private servicesService: DfBaseCrudService - ) {} + ) { + this.generateApiCardsData = [ + { + icon: this.faDatabase, + headerTextKey: 'home.welcomePage.connectToDatabaseCard.header', + textKey: 'home.welcomePage.connectToDatabaseCard.description', + route: this.fpDBRoute, + bgColor: 'rgba(127, 17, 224, 0.1)', + headerColor: 'rgb(127, 17, 224)', + }, + { + icon: this.faCode, + headerTextKey: 'home.welcomePage.useScriptingServicesCard.header', + textKey: 'home.welcomePage.useScriptingServicesCard.description', + route: this.fpScriptingRoute, + bgColor: 'rgba(92,35,154, 0.1)', + headerColor: 'rgb(92,35,154)', + }, + { + icon: this.faNetworkWired, + headerTextKey: 'home.welcomePage.proxyWebServicesCard.header', + textKey: 'home.welcomePage.proxyWebServicesCard.description', + route: this.fpNetworkRoute, + bgColor: 'rgb(235,253,245)', + headerColor: 'rgb(25,97,80)', + }, + { + icon: this.faFile, + headerTextKey: 'home.welcomePage.exposeFileStorageCard.header', + textKey: 'home.welcomePage.exposeFileStorageCard.description', + route: this.fpFileRoute, + bgColor: 'rgb(255,251,236)', + headerColor: 'rgb(136,72,43)', + }, + { + icon: this.faTools, + headerTextKey: 'home.welcomePage.utilityServicesCard.header', + textKey: 'home.welcomePage.utilityServicesCard.description', + route: this.fpUtilityRoute, + bgColor: 'rgba(80,105,137, 0.1)', + headerColor: 'rgb(80,105,137)', + }, + { + icon: this.faBook, + headerTextKey: 'home.welcomePage.apiDocsCard.header', + textKey: 'home.welcomePage.apiDocsCard.description', + route: this.fpApiDocsRoute, + bgColor: 'rgba(217, 54, 138, 0.1)', + headerColor: 'rgb(217, 54, 138)', + }, + ]; + } isDarkMode = this.themeService.darkMode$; isFirstTimeUser$ = this.storageService.isFirstTimeUser$; releases: any[] = []; diff --git a/src/app/adf-limits/df-manage-limits/df-manage-limits.component.html b/src/app/adf-limits/df-manage-limits/df-manage-limits.component.html index f8c7bb58..30ac6dea 100644 --- a/src/app/adf-limits/df-manage-limits/df-manage-limits.component.html +++ b/src/app/adf-limits/df-manage-limits/df-manage-limits.component.html @@ -1,4 +1,4 @@ - + diff --git a/src/app/adf-reports/df-manage-service-report/df-manage-service-report.component.html b/src/app/adf-reports/df-manage-service-report/df-manage-service-report.component.html index be87d3e1..ce7fa7f2 100644 --- a/src/app/adf-reports/df-manage-service-report/df-manage-service-report.component.html +++ b/src/app/adf-reports/df-manage-service-report/df-manage-service-report.component.html @@ -1,4 +1,6 @@ - + diff --git a/src/app/adf-roles/df-manage-roles/df-manage-roles-table.component.ts b/src/app/adf-roles/df-manage-roles/df-manage-roles-table.component.ts index d24338c8..4a317908 100644 --- a/src/app/adf-roles/df-manage-roles/df-manage-roles-table.component.ts +++ b/src/app/adf-roles/df-manage-roles/df-manage-roles-table.component.ts @@ -95,9 +95,9 @@ export class DfManageRolesTableComponent extends DfManageTableComponent }); } - refreshTable(limit?: number, offset?: number): void { + refreshTable(limit?: number, offset?: number, filter?: string): void { this.roleService - .getAll>({ limit, offset }) + .getAll>({ limit, offset, filter }) .subscribe(data => { this.dataSource.data = this.mapDataToTable(data.resource); this.tableLength = data.meta.count; diff --git a/src/app/adf-scheduler/df-manage-scheduler/df-manage-scheduler.component.html b/src/app/adf-scheduler/df-manage-scheduler/df-manage-scheduler.component.html index dad2f333..a5cc55a6 100644 --- a/src/app/adf-scheduler/df-manage-scheduler/df-manage-scheduler.component.html +++ b/src/app/adf-scheduler/df-manage-scheduler/df-manage-scheduler.component.html @@ -1,4 +1,6 @@ - + diff --git a/src/app/adf-schema/df-relationship-details/df-relationship-details.component.html b/src/app/adf-schema/df-relationship-details/df-relationship-details.component.html index c6891722..8f6bc00f 100644 --- a/src/app/adf-schema/df-relationship-details/df-relationship-details.component.html +++ b/src/app/adf-schema/df-relationship-details/df-relationship-details.component.html @@ -83,7 +83,7 @@ - {{ option.label }} + {{ option.name }} src.name) - .join('","')}"))`; + filter = `${ + filter ? `(${filter}) and ` : '' + }(type in ("${this.serviceTypes.map(src => src.name).join('","')}"))`; } - filter = `${filter ? `${filter} and ` : ''}(created_by_id is${ - !this.system ? ' not ' : ' ' - }null)`; this.serviceService .getAll>({ diff --git a/src/app/adf-services/df-manage-services/df-manage-services.component.html b/src/app/adf-services/df-manage-services/df-manage-services.component.html index 9da55379..426e1ea0 100644 --- a/src/app/adf-services/df-manage-services/df-manage-services.component.html +++ b/src/app/adf-services/df-manage-services/df-manage-services.component.html @@ -1,4 +1,6 @@ - + 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 7ca92eb9..62b448fd 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 @@ -87,7 +87,10 @@

- @@ -569,7 +572,11 @@

Full Access

{{ 'services.controls.serviceType.label' | transloco }} - + {{ type.label }} @@ -821,4 +828,7 @@

Full Access

- + + 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 9ab190fd..58695482 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 @@ -3,6 +3,7 @@ import { Component, ElementRef, Inject, + Input, OnInit, ViewChild, } from '@angular/core'; @@ -85,6 +86,9 @@ 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 { @@ -116,6 +120,14 @@ interface CategorizedField { advanced: ConfigSchema[]; } +interface ServiceResponse { + resource: Array<{ + id: number; + name: string; + [key: string]: any; + }>; +} + @UntilDestroy({ checkProperties: true }) @Component({ selector: 'df-service-details', @@ -167,6 +179,7 @@ export class DfServiceDetailsComponent implements OnInit { serviceForm: FormGroup; faCircleInfo = faCircleInfo; serviceData: Service; + selectedServiceTypeLable: string; configSchema: Array; images: Array; search = ''; @@ -800,8 +813,10 @@ export class DfServiceDetailsComponent implements OnInit { stepper.next(); } - openDialog() { - const dialogRef = this.dialog.open(DfPaywallModal); + openDialog(serviceTypeName: string) { + const dialogRef = this.dialog.open(DfPaywallModal, { + data: { serviceName: serviceTypeName }, + }); dialogRef.afterClosed().subscribe(); } @@ -1189,42 +1204,19 @@ export class DfServiceDetailsComponent implements OnInit { onAccessLevelSelect(level: string) { this.selectedAccessLevel = level; } + + getServiceTypeLabel(value: string): string { + const selectedType = this.serviceTypes.find(type => type.name === value); + return selectedType ? selectedType.label : value; + } + + onServiceTypeSelect(selectedServiceTypeLable: string) { + this.selectedServiceTypeLable = + selectedServiceTypeLable || 'Unknown. Unable to identify Service Type'; + } } interface ImageObject { alt: string; src: string; label: string; } - -@Component({ - selector: 'df-paywall-modal', - templateUrl: 'df-paywall-modal.html', - styleUrls: ['./df-service-details.component.scss'], - standalone: true, - imports: [ - MatDialogModule, - MatButtonModule, - DfPaywallComponent, - TranslocoPipe, - ], -}) -// eslint-disable-next-line @angular-eslint/component-class-suffix -export class DfPaywallModal { - @ViewChild('calendlyWidget') calendlyWidget: ElementRef; - - ngAfterViewInit(): void { - (window as any)['Calendly'].initInlineWidget({ - url: 'https://calendly.com/dreamfactory-platform/unlock-all-features', - parentElement: this.calendlyWidget.nativeElement, - autoLoad: false, - }); - } -} - -interface ServiceResponse { - resource: Array<{ - id: number; - name: string; - [key: string]: any; - }>; -} diff --git a/src/app/ai/ai.component.html b/src/app/ai/ai.component.html new file mode 100644 index 00000000..e381556d --- /dev/null +++ b/src/app/ai/ai.component.html @@ -0,0 +1,62 @@ +
+
+
+

AI Gateway Data Platform

+
+

+ Unlock the power of AI with your data! Our upcoming AI capabilities + will enable you to: +

+
    +
  • + ✨ Secure Dataset Exposure: Safely expose your + datasets to AI clients with full RBAC protections +
  • +
  • + 🔐 Enterprise-Grade Security: Maintain complete + control over data access and permissions +
  • +
  • + 🚀 Seamless Integration: Connect popular AI + platforms and tools directly to your DreamFactory APIs +
  • +
  • + 📊 Intelligent Analytics: Generate insights and + recommendations powered by machine learning +
  • +
+
+

🎯 Ready to Get Started?

+

+ Contact us below to join our exclusive AI beta program and be among + the first to experience these cutting-edge capabilities! +

+
+
+
+
+ +
+ +
+

AI Assistant

+

+ Welcome to the AI section! This is where AI-powered features will be + implemented. +

+
+
+

Smart Analytics

+

AI-powered data insights and analytics

+
+
+

Automated Tasks

+

Intelligent automation and task management

+
+
+

Predictive Modeling

+

Advanced machine learning predictions

+
+
+
+
diff --git a/src/app/ai/ai.component.scss b/src/app/ai/ai.component.scss new file mode 100644 index 00000000..ad745f32 --- /dev/null +++ b/src/app/ai/ai.component.scss @@ -0,0 +1,155 @@ +.ai-paywall-container { + min-height: 100vh; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); +} + +.ai-intro-section { + padding: 3rem 2rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + position: relative; + overflow: hidden; +} + +.ai-intro-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.3; +} + +.ai-intro-content { + max-width: 800px; + margin: 0 auto; + position: relative; + z-index: 1; +} + +.ai-title { + font-size: 2.5rem; + font-weight: 700; + margin-bottom: 1.5rem; + text-align: center; + color: #000; +} + +@keyframes rainbow-text { + 0%, + 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } +} + +.lead-text { + font-size: 1.2rem; + font-weight: 500; + margin-bottom: 2rem; + text-align: center; + opacity: 0.95; +} + +.feature-list { + list-style: none; + padding: 0; + margin: 2rem 0; +} + +.feature-list li { + padding: 0.8rem 0; + font-size: 1.1rem; + display: flex; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + opacity: 0.9; +} + +.feature-list li:last-child { + border-bottom: none; +} + +.beta-callout { + background: rgba(255, 255, 255, 0.1); + padding: 1.5rem; + border-radius: 12px; + margin-top: 2rem; + text-align: center; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.beta-callout h3 { + margin-bottom: 0.5rem; + font-size: 1.3rem; + color: #ffd700; +} + +.beta-callout p { + margin: 0; + font-size: 1rem; + opacity: 0.9; +} + +.ai-container { + padding: 2rem; + max-width: 1200px; + margin: 0 auto; +} + +h1 { + color: #333; + margin-bottom: 1rem; + background: linear-gradient( + 45deg, + #dc143c, + #ff4500, + #ffa500, + #32cd32, + #1e90ff, + #8a2be2 + ); + background-size: 300% 300%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: rainbow-text 3s ease-in-out infinite; + font-weight: 700; + text-shadow: 0 0 15px rgba(0, 0, 0, 0.2); + filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.1)); +} + +.ai-content { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-top: 2rem; +} + +.feature-card { + background: #f8f9fa; + padding: 1.5rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.2s ease; +} + +.feature-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.feature-card h3 { + margin-bottom: 0.5rem; + color: #333; +} + +.feature-card p { + color: #666; + line-height: 1.5; +} diff --git a/src/app/ai/ai.component.ts b/src/app/ai/ai.component.ts new file mode 100644 index 00000000..446d9219 --- /dev/null +++ b/src/app/ai/ai.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute } from '@angular/router'; +import { DfPaywallComponent } from '../shared/components/df-paywall/df-paywall.component'; +import { NgIf } from '@angular/common'; + +@Component({ + selector: 'app-ai', + standalone: true, + imports: [CommonModule, DfPaywallComponent, NgIf], + templateUrl: './ai.component.html', + styleUrls: ['./ai.component.scss'], +}) +export class AiComponent { + paywall = false; + + constructor(private activatedRoute: ActivatedRoute) { + this.activatedRoute.data.subscribe(({ showPaywall }) => { + if (showPaywall) { + this.paywall = true; + } + }); + } +} diff --git a/src/app/routes.ts b/src/app/routes.ts index ae260047..ad9733f1 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -799,6 +799,12 @@ export const routes: Routes = [ ], canActivate: [loggedInGuard, licenseGuard], }, + { + path: ROUTES.AI, + loadComponent: () => import('./ai/ai.component').then(m => m.AiComponent), + canActivate: [loggedInGuard, licenseGuard], + data: { showPaywall: true }, + }, { path: ROUTES.PROFILE, loadComponent: () => diff --git a/src/app/adf-services/df-service-details/df-paywall-modal.html b/src/app/shared/components/df-paywall-modal/df-paywall-modal.component.html similarity index 100% rename from src/app/adf-services/df-service-details/df-paywall-modal.html rename to src/app/shared/components/df-paywall-modal/df-paywall-modal.component.html diff --git a/src/app/shared/components/df-paywall-modal/df-paywall-modal.component.scss b/src/app/shared/components/df-paywall-modal/df-paywall-modal.component.scss new file mode 100644 index 00000000..8b321e56 --- /dev/null +++ b/src/app/shared/components/df-paywall-modal/df-paywall-modal.component.scss @@ -0,0 +1 @@ +/* Styles for DfPaywallModal. Add or move specific styles here if needed. */ diff --git a/src/app/shared/components/df-paywall-modal/df-paywall-modal.component.ts b/src/app/shared/components/df-paywall-modal/df-paywall-modal.component.ts new file mode 100644 index 00000000..16845c1d --- /dev/null +++ b/src/app/shared/components/df-paywall-modal/df-paywall-modal.component.ts @@ -0,0 +1,53 @@ +import { + Component, + ElementRef, + ViewChild, + OnInit, + AfterViewInit, + Inject, +} from '@angular/core'; +import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { DfPaywallComponent } from '../df-paywall/df-paywall.component'; +import { TranslocoPipe } from '@ngneat/transloco'; +import { DfUserDataService } from '../../services/df-user-data.service'; +import { DfSystemConfigDataService } from '../../services/df-system-config-data.service'; +import { DfPaywallService } from '../../services/df-paywall.service'; + +@Component({ + selector: 'df-paywall-modal', + templateUrl: './df-paywall-modal.component.html', + styleUrls: ['./df-paywall-modal.component.scss'], + standalone: true, + imports: [ + MatDialogModule, + MatButtonModule, + DfPaywallComponent, + TranslocoPipe, + ], +}) +export class DfPaywallModal implements OnInit, AfterViewInit { + @ViewChild('calendlyWidget') calendlyWidget: ElementRef; + + constructor( + private userDataService: DfUserDataService, + private systemConfigService: DfSystemConfigDataService, + private dfPaywallService: DfPaywallService, + @Inject(MAT_DIALOG_DATA) public data: { serviceName: string } + ) {} + + ngOnInit(): void { + const user = this.userDataService.userData; + const email = user?.email; + const ip = this.systemConfigService?.environment?.client?.ipAddress; + this.dfPaywallService.trackPaywallHit(email, ip, this.data.serviceName); + } + + ngAfterViewInit(): void { + (window as any)['Calendly'].initInlineWidget({ + url: 'https://calendly.com/dreamfactory-platform/unlock-all-features', + parentElement: this.calendlyWidget.nativeElement, + autoLoad: false, + }); + } +} diff --git a/src/app/shared/components/df-paywall/df-paywall.component.ts b/src/app/shared/components/df-paywall/df-paywall.component.ts index 4600511a..f87122e0 100644 --- a/src/app/shared/components/df-paywall/df-paywall.component.ts +++ b/src/app/shared/components/df-paywall/df-paywall.component.ts @@ -1,5 +1,15 @@ -import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; +import { + AfterViewInit, + Component, + ElementRef, + ViewChild, + Input, + OnInit, +} from '@angular/core'; import { TranslocoPipe } from '@ngneat/transloco'; +import { DfUserDataService } from '../../services/df-user-data.service'; +import { DfSystemConfigDataService } from '../../services/df-system-config-data.service'; +import { DfPaywallService } from '../../services/df-paywall.service'; @Component({ selector: 'df-paywall', @@ -8,8 +18,24 @@ import { TranslocoPipe } from '@ngneat/transloco'; standalone: true, imports: [TranslocoPipe], }) -export class DfPaywallComponent implements AfterViewInit { +export class DfPaywallComponent implements AfterViewInit, OnInit { @ViewChild('calendlyWidget') calendlyWidget: ElementRef; + @Input() serviceName: string; + + constructor( + private userDataService: DfUserDataService, + private systemConfigService: DfSystemConfigDataService, + private dfPaywallService: DfPaywallService + ) {} + + ngOnInit(): void { + const user = this.userDataService.userData; + const email = user?.email; + const ip = this.systemConfigService?.environment?.client?.ipAddress; + const serviceName = this.serviceName; + + this.dfPaywallService.trackPaywallHit(email, ip, serviceName); + } ngAfterViewInit(): void { (window as any)['Calendly'].initInlineWidget({ diff --git a/src/app/shared/components/df-side-nav/df-side-nav.component.html b/src/app/shared/components/df-side-nav/df-side-nav.component.html index c8ded1a4..2a1e316e 100644 --- a/src/app/shared/components/df-side-nav/df-side-nav.component.html +++ b/src/app/shared/components/df-side-nav/df-side-nav.component.html @@ -154,6 +154,7 @@

class="nav-item" [class.active]="isActive(item)" [class.commercial-feature]="isFeatureLocked(item.path, licenseType)" + [class.ai-nav-item]="item.path === '/ai'" (click)="handleNavClick(item)"> diff --git a/src/app/shared/components/df-side-nav/df-side-nav.component.scss b/src/app/shared/components/df-side-nav/df-side-nav.component.scss index 3d3ebe4f..7ce4ff17 100644 --- a/src/app/shared/components/df-side-nav/df-side-nav.component.scss +++ b/src/app/shared/components/df-side-nav/df-side-nav.component.scss @@ -159,6 +159,126 @@ $red-palette: mat.define-palette(mat.$red-palette); ::ng-deep .mat-mdc-button-touch-target { background-color: #f6f2fa; } + + // Special styling for AI navigation item + &.ai-nav-item { + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(220, 20, 60, 0.15), + rgba(255, 69, 0, 0.15), + rgba(255, 165, 0, 0.15), + rgba(50, 205, 50, 0.15), + rgba(30, 144, 255, 0.15), + rgba(138, 43, 226, 0.15), + transparent + ); + animation: rainbow-slide 3s ease-in-out infinite; + } + + span { + background: linear-gradient( + 45deg, + #dc143c, + #ff4500, + #ffa500, + #32cd32, + #1e90ff, + #8a2be2 + ); + background-size: 300% 300%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: rainbow-text 2s ease-in-out infinite; + font-weight: 700; + text-shadow: 0 0 15px rgba(0, 0, 0, 0.3); + filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.2)); + } + + img { + filter: hue-rotate(0deg) saturate(2) brightness(0.8) contrast(1.3); + animation: rainbow-icon 4s linear infinite; + drop-shadow: 0 0 6px rgba(0, 0, 0, 0.3); + } + + &:hover { + &::before { + animation-duration: 1s; + } + + span { + animation-duration: 1s; + } + + img { + animation-duration: 2s; + } + } + } + + @keyframes rainbow-slide { + 0% { + left: -100%; + } + 50% { + left: 100%; + } + 100% { + left: -100%; + } + } + + @keyframes rainbow-text { + 0%, + 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + } + + @keyframes rainbow-icon { + 0% { + filter: hue-rotate(0deg) saturate(2) brightness(0.8) contrast(1.3) + drop-shadow(0 0 6px rgba(220, 20, 60, 0.4)); + } + 16.66% { + filter: hue-rotate(60deg) saturate(2.2) brightness(0.7) contrast(1.4) + drop-shadow(0 0 6px rgba(255, 69, 0, 0.4)); + } + 33.33% { + filter: hue-rotate(120deg) saturate(2.4) brightness(0.6) contrast(1.5) + drop-shadow(0 0 6px rgba(255, 165, 0, 0.4)); + } + 50% { + filter: hue-rotate(180deg) saturate(2.2) brightness(0.7) contrast(1.4) + drop-shadow(0 0 6px rgba(50, 205, 50, 0.4)); + } + 66.66% { + filter: hue-rotate(240deg) saturate(2) brightness(0.8) contrast(1.3) + drop-shadow(0 0 6px rgba(30, 144, 255, 0.4)); + } + 83.33% { + filter: hue-rotate(300deg) saturate(2.2) brightness(0.7) contrast(1.4) + drop-shadow(0 0 6px rgba(138, 43, 226, 0.4)); + } + 100% { + filter: hue-rotate(360deg) saturate(2) brightness(0.8) contrast(1.3) + drop-shadow(0 0 6px rgba(220, 20, 60, 0.4)); + } + } &.active { ::ng-deep .mat-mdc-button-touch-target { background-color: #e3dfff; diff --git a/src/app/shared/services/df-paywall.service.ts b/src/app/shared/services/df-paywall.service.ts index 802bab81..b814d75c 100644 --- a/src/app/shared/services/df-paywall.service.ts +++ b/src/app/shared/services/df-paywall.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { catchError, map, of, switchMap } from 'rxjs'; import { DfSystemConfigDataService } from './df-system-config-data.service'; import { DfErrorService } from './df-error.service'; +import { HttpClient } from '@angular/common/http'; @Injectable({ providedIn: 'root', @@ -27,7 +28,8 @@ export class DfPaywallService { constructor( private systemConfigDataService: DfSystemConfigDataService, - private errorService: DfErrorService + private errorService: DfErrorService, + private http: HttpClient ) {} activatePaywall(resource?: string | Array) { @@ -60,4 +62,23 @@ export class DfPaywallService { return of(false); } } + + trackPaywallHit( + email: string = 'Unknown. Unable to fetch email', + ip_address: string = 'Unknown. Unable to fetch IP address', + service_name: string = 'Service name is not specified' + ): void { + this.http + .post('https://updates.dreamfactory.com/api/paywall', { + email, + ip_address: ip_address, + service_name: service_name, + }) + .subscribe({ + next: () => {}, + error: err => { + console.error('Paywall tracking failed:', err); + }, + }); + } } diff --git a/src/app/shared/services/df-user-data.service.ts b/src/app/shared/services/df-user-data.service.ts index a6aba456..78ebb037 100644 --- a/src/app/shared/services/df-user-data.service.ts +++ b/src/app/shared/services/df-user-data.service.ts @@ -60,6 +60,10 @@ export class DfUserDataService { this.isLoggedIn = false; } + get userData(): UserSession | null { + return this.userDataSubject.value; + } + set userData(userData: UserSession | null) { this.userDataSubject.next(userData); if (userData) { diff --git a/src/app/shared/types/files.ts b/src/app/shared/types/files.ts index a6e25dba..ba4c0541 100644 --- a/src/app/shared/types/files.ts +++ b/src/app/shared/types/files.ts @@ -36,3 +36,24 @@ export interface FileType { } type EntityType = 'file' | 'folder'; + +export interface ApiDocJson { + info: { + description?: string; + title: string; + version?: string; + group: string; + }; + paths: { + [endpoint: string]: { + [method: string]: { + operationId: string; + description: string; + summary: string; + tags: string[]; + [key: string]: any; + }; + }; + }; + [key: string]: any; +} diff --git a/src/app/shared/types/routes.ts b/src/app/shared/types/routes.ts index 768263d9..3794b4b8 100644 --- a/src/app/shared/types/routes.ts +++ b/src/app/shared/types/routes.ts @@ -16,6 +16,7 @@ export enum ROUTES { QUICKSTART = 'quickstart', RESOURCES = 'resources', DOWNLOAD = 'download', + AI = 'ai', API_CONNECTIONS = 'api-connections', API_TYPES = 'api-types', DATABASE = 'database', diff --git a/src/app/shared/utilities/route.ts b/src/app/shared/utilities/route.ts index 3ea19119..5b04e1c1 100644 --- a/src/app/shared/utilities/route.ts +++ b/src/app/shared/utilities/route.ts @@ -15,6 +15,7 @@ const filteredFromNav = [ const navIcons = [ 'home', + 'ai', 'admin-settings', 'api-connections', 'api-security', @@ -62,7 +63,7 @@ export function accessibleRoutes( navs: Array