This is an automated email from the ASF dual-hosted git repository.

mcgilman pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new feb72efc23 [NIFI-14648] - Skeleton/Spinner in Import from Registry 
dialog (#10005)
feb72efc23 is described below

commit feb72efc23d40416c68642ae9516b150f23d8faa
Author: Rob Fellows <[email protected]>
AuthorDate: Mon Jun 16 11:28:07 2025 -0400

    [NIFI-14648] - Skeleton/Spinner in Import from Registry dialog (#10005)
---
 .../import-from-registry.component.html            | 111 ++++++++++++---------
 .../import-from-registry.component.spec.ts         |  33 ++++++
 .../import-from-registry.component.ts              |  88 ++++++++++++++--
 3 files changed, 177 insertions(+), 55 deletions(-)

diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.html
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.html
index 12801cea5e..9bd696f254 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.html
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.html
@@ -119,57 +119,74 @@
                     </div>
                 </div>
             </div>
-            <div class="listing-table flex-1 relative min-h-48">
-                <div class="absolute inset-0 overflow-y-auto 
overflow-x-hidden">
-                    <table
-                        mat-table
-                        [dataSource]="dataSource"
-                        matSort
-                        matSortDisableClear
-                        (matSortChange)="sortData($event)"
-                        [matSortActive]="sort.active"
-                        [matSortDirection]="sort.direction">
-                        <!-- Version Column -->
-                        <ng-container matColumnDef="version">
-                            <th mat-header-cell *matHeaderCellDef 
mat-sort-header>Version</th>
-                            <td mat-cell *matCellDef="let item">
-                                <div class="overflow-ellipsis overflow-hidden 
whitespace-nowrap" [title]="item.version">
-                                    {{ item.version }}
-                                </div>
-                            </td>
-                        </ng-container>
 
-                        <!-- Create Column -->
-                        <ng-container matColumnDef="created">
-                            <th mat-header-cell *matHeaderCellDef 
mat-sort-header>Created</th>
-                            <td mat-cell *matCellDef="let item">
-                                {{ formatTimestamp(item) }}
-                            </td>
-                        </ng-container>
+            @if (loadingVersions()) {
+                <div data-qa="skeleton-loader-versions" class="w-full">
+                    <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+                </div>
+            } @else if (loadingVersionsError()) {
+                <div
+                    class="flex flex-1 flex-col gap-y-4 justify-center 
items-center tertiary-color pb-4"
+                    data-qa="loading-versions-error">
+                    <i class="fa fa-exclamation-circle error-color fa-4x"></i>
+                    <div class="text-lg font-semibold">Something went 
wrong</div>
+                    <div class="text-center">{{ loadingVersionsError() }}</div>
+                </div>
+            } @else {
+                <div class="listing-table flex-1 relative min-h-48" 
data-qa="versions-listing-table">
+                    <div class="absolute inset-0 overflow-y-auto 
overflow-x-hidden">
+                        <table
+                            mat-table
+                            [dataSource]="dataSource"
+                            matSort
+                            matSortDisableClear
+                            (matSortChange)="sortData($event)"
+                            [matSortActive]="sort.active"
+                            [matSortDirection]="sort.direction">
+                            <!-- Version Column -->
+                            <ng-container matColumnDef="version">
+                                <th mat-header-cell *matHeaderCellDef 
mat-sort-header>Version</th>
+                                <td mat-cell *matCellDef="let item">
+                                    <div
+                                        class="overflow-ellipsis 
overflow-hidden whitespace-nowrap"
+                                        [title]="item.version">
+                                        {{ item.version }}
+                                    </div>
+                                </td>
+                            </ng-container>
 
-                        <!-- Comments Column -->
-                        <ng-container matColumnDef="comments">
-                            <th mat-header-cell *matHeaderCellDef 
mat-sort-header>Comments</th>
-                            <td mat-cell *matCellDef="let item">
-                                <div
-                                    class="overflow-ellipsis overflow-hidden 
whitespace-nowrap"
-                                    [title]="item.comments">
-                                    {{ item.comments }}
-                                </div>
-                            </td>
-                        </ng-container>
+                            <!-- Create Column -->
+                            <ng-container matColumnDef="created">
+                                <th mat-header-cell *matHeaderCellDef 
mat-sort-header>Created</th>
+                                <td mat-cell *matCellDef="let item">
+                                    {{ formatTimestamp(item) }}
+                                </td>
+                            </ng-container>
+
+                            <!-- Comments Column -->
+                            <ng-container matColumnDef="comments">
+                                <th mat-header-cell *matHeaderCellDef 
mat-sort-header>Comments</th>
+                                <td mat-cell *matCellDef="let item">
+                                    <div
+                                        class="overflow-ellipsis 
overflow-hidden whitespace-nowrap"
+                                        [title]="item.comments">
+                                        {{ item.comments }}
+                                    </div>
+                                </td>
+                            </ng-container>
 
-                        <tr mat-header-row *matHeaderRowDef="displayedColumns; 
sticky: true"></tr>
-                        <tr
-                            mat-row
-                            *matRowDef="let row; let even = even; columns: 
displayedColumns"
-                            (click)="select(row)"
-                            (dblclick)="importFromRegistry()"
-                            [class.selected]="isSelected(row)"
-                            [class.even]="even"></tr>
-                    </table>
+                            <tr mat-header-row 
*matHeaderRowDef="displayedColumns; sticky: true"></tr>
+                            <tr
+                                mat-row
+                                *matRowDef="let row; let even = even; columns: 
displayedColumns"
+                                (click)="select(row)"
+                                (dblclick)="importFromRegistry()"
+                                [class.selected]="isSelected(row)"
+                                [class.even]="even"></tr>
+                        </table>
+                    </div>
                 </div>
-            </div>
+            }
         </div>
     </mat-dialog-content>
     <mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as 
saving">
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.spec.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.spec.ts
index 82c5fcc3a9..a5e7327107 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.spec.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.spec.ts
@@ -26,6 +26,7 @@ import { initialState } from 
'../../../../../state/flow/flow.reducer';
 import { NoopAnimationsModule } from '@angular/platform-browser/animations';
 import { EMPTY } from 'rxjs';
 import { ClusterConnectionService } from 
'../../../../../../../service/cluster-connection.service';
+import { By } from '@angular/platform-browser';
 
 describe('ImportFromRegistry', () => {
     let component: ImportFromRegistry;
@@ -144,4 +145,36 @@ describe('ImportFromRegistry', () => {
     it('should create', () => {
         expect(component).toBeTruthy();
     });
+
+    it('should show the skeleton loader for versions', () => {
+        component.loadingVersions.set(true);
+        fixture.detectChanges();
+        const skeleton = 
fixture.debugElement.query(By.css('div[data-qa="skeleton-loader-versions"]'));
+        const error = 
fixture.debugElement.query(By.css('div[data-qa="loading-versions-error"]'));
+        expect(skeleton).toBeTruthy();
+        expect(error).toBeFalsy();
+    });
+
+    it('should show the loading error panel if there is an error', () => {
+        component.loadingVersions.set(false);
+        component.loadingVersionsError.set('some error happened');
+        fixture.detectChanges();
+        const skeleton = 
fixture.debugElement.query(By.css('div[data-qa="skeleton-loader-versions"]'));
+        const error = 
fixture.debugElement.query(By.css('div[data-qa="loading-versions-error"]'));
+        const versions = 
fixture.debugElement.query(By.css('div[data-qa="versions-listing-table"]'));
+        expect(skeleton).toBeFalsy();
+        expect(error).toBeTruthy();
+        expect(versions).toBeFalsy();
+    });
+
+    it('should show the versions', () => {
+        component.loadingVersions.set(false);
+        fixture.detectChanges();
+        const skeleton = 
fixture.debugElement.query(By.css('div[data-qa="skeleton-loader-versions"]'));
+        const error = 
fixture.debugElement.query(By.css('div[data-qa="loading-versions-error"]'));
+        const versions = 
fixture.debugElement.query(By.css('div[data-qa="versions-listing-table"]'));
+        expect(skeleton).toBeFalsy();
+        expect(error).toBeFalsy();
+        expect(versions).toBeTruthy();
+    });
 });
diff --git 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts
 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts
index 8952e8e70b..00167b19a5 100644
--- 
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts
+++ 
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/import-from-registry/import-from-registry.component.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { Component, Inject, Input, OnInit } from '@angular/core';
+import { Component, Inject, Input, OnInit, signal, WritableSignal } from 
'@angular/core';
 import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
 import { ImportFromRegistryDialogRequest } from '../../../../../state/flow';
 import { Store } from '@ngrx/store';
@@ -39,7 +39,7 @@ import { MatSelectModule } from '@angular/material/select';
 import { NifiSpinnerDirective } from 
'../../../../../../../ui/common/spinner/nifi-spinner.directive';
 import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators 
} from '@angular/forms';
 import { MatIconModule } from '@angular/material/icon';
-import { Observable, of, take } from 'rxjs';
+import { catchError, EMPTY, Observable, of, take } from 'rxjs';
 import { MatCheckboxModule } from '@angular/material/checkbox';
 import { MatTableDataSource, MatTableModule } from '@angular/material/table';
 import { MatSortModule, Sort } from '@angular/material/sort';
@@ -58,6 +58,9 @@ import { importFromRegistry } from 
'../../../../../state/flow/flow.actions';
 import { ClusterConnectionService } from 
'../../../../../../../service/cluster-connection.service';
 import { ErrorContextKey } from '../../../../../../../state/error';
 import { ContextErrorBanner } from 
'../../../../../../../ui/common/context-error-banner/context-error-banner.component';
+import { ErrorHelper } from 
'../../../../../../../service/error-helper.service';
+import { HttpErrorResponse } from '@angular/common/http';
+import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
 
 @Component({
     selector: 'import-from-registry',
@@ -78,7 +81,8 @@ import { ContextErrorBanner } from 
'../../../../../../../ui/common/context-error
         MatCheckboxModule,
         MatSortModule,
         MatTableModule,
-        ContextErrorBanner
+        ContextErrorBanner,
+        NgxSkeletonLoaderComponent
     ],
     templateUrl: './import-from-registry.component.html',
     styleUrls: ['./import-from-registry.component.scss']
@@ -123,13 +127,20 @@ export class ImportFromRegistry extends 
CloseOnEscapeDialog implements OnInit {
         new MatTableDataSource<VersionedFlowSnapshotMetadata>();
     selectedFlowVersion: string | null = null;
 
+    loadingBranches: WritableSignal<boolean> = signal(false);
+    loadingBuckets: WritableSignal<boolean> = signal(false);
+    loadingFlows: WritableSignal<boolean> = signal(false);
+    loadingVersions: WritableSignal<boolean> = signal(false);
+    loadingVersionsError: WritableSignal<string | null> = signal(null);
+
     constructor(
         @Inject(MAT_DIALOG_DATA) private dialogRequest: 
ImportFromRegistryDialogRequest,
         private formBuilder: FormBuilder,
         private store: Store<CanvasState>,
         private nifiCommon: NiFiCommon,
         private client: Client,
-        private clusterConnectionService: ClusterConnectionService
+        private clusterConnectionService: ClusterConnectionService,
+        private errorHelper: ErrorHelper
     ) {
         super();
         this.store
@@ -230,12 +241,47 @@ export class ImportFromRegistry extends 
CloseOnEscapeDialog implements OnInit {
         this.loadVersions(registryId, bucketId, flowId, branch);
     }
 
+    private setLoading(resourceType: string, loading: boolean): void {
+        let formControl;
+        switch (resourceType) {
+            case 'branch':
+                this.loadingBranches.set(loading);
+                formControl = this.importFromRegistryForm.get('branch');
+                break;
+            case 'bucket':
+                this.loadingBuckets.set(loading);
+                formControl = this.importFromRegistryForm.get('bucket');
+                break;
+            case 'flow':
+                this.loadingFlows.set(loading);
+                formControl = this.importFromRegistryForm.get('flow');
+                break;
+            case 'version':
+                this.loadingVersions.set(loading);
+                break;
+        }
+        if (formControl) {
+            if (loading) {
+                formControl.disable();
+            } else {
+                formControl.enable();
+            }
+        }
+    }
+
     loadBranches(registryId: string): void {
         if (registryId) {
+            this.setLoading('branch', true);
             this.branchOptions = [];
 
             this.getBranches(registryId)
-                .pipe(take(1))
+                .pipe(
+                    take(1),
+                    catchError(() => {
+                        this.setLoading('branch', false);
+                        return EMPTY;
+                    })
+                )
                 .subscribe((branches: BranchEntity[]) => {
                     if (branches.length > 0) {
                         branches.forEach((entity: BranchEntity) => {
@@ -251,15 +297,23 @@ export class ImportFromRegistry extends 
CloseOnEscapeDialog implements OnInit {
                             this.loadBuckets(registryId, branchId);
                         }
                     }
+                    this.setLoading('branch', false);
                 });
         }
     }
 
     loadBuckets(registryId: string, branch?: string | null): void {
+        this.setLoading('bucket', true);
         this.bucketOptions = [];
 
         this.getBuckets(registryId, branch)
-            .pipe(take(1))
+            .pipe(
+                take(1),
+                catchError(() => {
+                    this.setLoading('bucket', false);
+                    return EMPTY;
+                })
+            )
             .subscribe((buckets: BucketEntity[]) => {
                 if (buckets.length > 0) {
                     buckets.forEach((entity: BucketEntity) => {
@@ -278,15 +332,23 @@ export class ImportFromRegistry extends 
CloseOnEscapeDialog implements OnInit {
                         this.loadFlows(registryId, bucketId, branch);
                     }
                 }
+                this.setLoading('bucket', false);
             });
     }
 
     loadFlows(registryId: string, bucketId: string, branch?: string | null): 
void {
+        this.setLoading('flow', true);
         this.flowOptions = [];
         this.flowLookup.clear();
 
         this.getFlows(registryId, bucketId, branch)
-            .pipe(take(1))
+            .pipe(
+                take(1),
+                catchError(() => {
+                    this.setLoading('flow', false);
+                    return EMPTY;
+                })
+            )
             .subscribe((versionedFlows: VersionedFlowEntity[]) => {
                 if (versionedFlows.length > 0) {
                     versionedFlows.forEach((entity: VersionedFlowEntity) => {
@@ -305,16 +367,25 @@ export class ImportFromRegistry extends 
CloseOnEscapeDialog implements OnInit {
                         this.loadVersions(registryId, bucketId, flowId, 
branch);
                     }
                 }
+                this.setLoading('flow', false);
             });
     }
 
     loadVersions(registryId: string, bucketId: string, flowId: string, 
branch?: string | null): void {
+        this.setLoading('version', true);
         this.dataSource.data = [];
         this.selectedFlowVersion = null;
         this.selectedFlowDescription = 
this.flowLookup.get(flowId)?.description;
 
         this.getFlowVersions(registryId, bucketId, flowId, branch)
-            .pipe(take(1))
+            .pipe(
+                take(1),
+                catchError((errorResponse: HttpErrorResponse) => {
+                    this.setLoading('version', false);
+                    
this.loadingVersionsError.set(this.errorHelper.getErrorString(errorResponse));
+                    return EMPTY;
+                })
+            )
             .subscribe((metadataEntities: 
VersionedFlowSnapshotMetadataEntity[]) => {
                 if (metadataEntities.length > 0) {
                     const flowVersions = metadataEntities.map(
@@ -326,6 +397,7 @@ export class ImportFromRegistry extends CloseOnEscapeDialog 
implements OnInit {
 
                     this.dataSource.data = sortedFlowVersions;
                 }
+                this.setLoading('version', false);
             });
     }
 

Reply via email to