elcsiga commented on code in PR #9855: URL: https://github.com/apache/nifi/pull/9855#discussion_r2066972351
########## nifi-frontend/src/main/frontend/apps/nifi-registry/src/styles.scss: ########## @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@use '@angular/material' as mat; +@use 'sass:map'; + +@use 'libs/shared/src/assets/styles/app' as app; +@use 'app/app.component-theme' as app-component; +@use 'app/ui/header/header.component-theme' as header; + +@use 'font-awesome'; +@use 'libs/shared/src/assets/themes/material'; + + // Include the common styles for Angular Material. We include this here so that you only + // have to load a single css file for Angular Material in your app. + // Be sure that you only ever include this mixin once! + @include mat.core(); + + @tailwind base; + @tailwind components; + @tailwind utilities; + + // only include these once (not needed for dark mode) + @include app.styles(); + + html { + @include app.generate-material-theme(); Review Comment: ```suggestion @include app.generate-material-theme(); ``` ########## nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.effects.ts: ########## @@ -0,0 +1,280 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { inject, Injectable } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { MatDialog } from '@angular/material/dialog'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { concatLatestFrom } from '@ngrx/operators'; +import { catchError, from, map, of, switchMap, tap } from 'rxjs'; +import { MEDIUM_DIALOG, SMALL_DIALOG, XL_DIALOG } from '@nifi/shared'; +import { NiFiState } from '../index'; +import { DropletsService } from '../../service/droplets.service'; +import * as DropletsActions from './droplets.actions'; +import { DeleteDropletDialogComponent } from '../../pages/expolorer/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component'; +import { ImportNewFlowDialogComponent } from '../../pages/expolorer/feature/ui/import-new-flow-dialog/import-new-flow-dialog.component'; +import { ImportNewFlowVersionDialogComponent } from '../../pages/expolorer/feature/ui/import-new-flow-version-dialog/import-new-flow-version-dialog.component'; +import { ExportFlowVersionDialogComponent } from '../../pages/expolorer/feature/ui/export-flow-version-dialog/export-flow-version-dialog.component'; +import { FlowVersionsDialogComponent } from '../../pages/expolorer/feature/ui/flow-versions-dialog/flow-versions-dialog.component'; +import { ErrorHelper } from '../../service/error-helper.service'; +import * as ErrorActions from '../../state/error/error.actions'; +import { selectStatus } from './droplets.selectors'; + +@Injectable() +export class DropletsEffects { + constructor( + private store: Store<NiFiState>, + private dropletsService: DropletsService, + private dialog: MatDialog, + private errorHelper: ErrorHelper + ) {} + + actions$ = inject(Actions); + + loadDroplets$ = createEffect(() => + this.actions$.pipe( + ofType(DropletsActions.loadDroplets), + concatLatestFrom(() => this.store.select(selectStatus)), + switchMap(([, status]) => { + return from( + this.dropletsService.getDroplets().pipe( + map((response) => + DropletsActions.loadDropletsSuccess({ + response: { + droplets: response + } + }) + ), + catchError((errorResponse: HttpErrorResponse) => + of(this.errorHelper.handleLoadingError(status, errorResponse)) + ) + ) + ); + }) + ) + ); + + openDeleteDropletDialog$ = createEffect( + () => + this.actions$.pipe( + ofType(DropletsActions.openDeleteDropletDialog), + tap(({ request }) => { + this.dialog.open(DeleteDropletDialogComponent, { + ...SMALL_DIALOG, + autoFocus: false, + data: request + }); + }) + ), + { dispatch: false } + ); + + deleteDroplet$ = createEffect(() => + this.actions$.pipe( + ofType(DropletsActions.deleteDroplet), + map((action) => action.request), + switchMap((request) => + from(this.dropletsService.deleteDroplet(request.droplet.link.href)).pipe( + map((res) => DropletsActions.deleteDropletSuccess({ response: res })), + catchError((errorResponse: HttpErrorResponse) => + of(ErrorActions.snackBarError({ error: this.errorHelper.getErrorString(errorResponse) })) + ) + ) + ) + ) + ); + + deleteDropletSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(DropletsActions.deleteDropletSuccess), + switchMap(() => of(DropletsActions.loadDroplets())) + ) + ); + + openImportNewFlowDialog$ = createEffect( + () => + this.actions$.pipe( + ofType(DropletsActions.openImportNewFlowDialog), + map((action) => action.request), + tap((request) => { + this.dialog.open(ImportNewFlowDialogComponent, { + ...MEDIUM_DIALOG, + autoFocus: false, + data: { + activeBucket: request.activeBucket, + buckets: request.buckets + } + }); + }) + ), + { dispatch: false } + ); + + createNewFlow$ = createEffect(() => + this.actions$.pipe( + ofType(DropletsActions.createNewFlow), + map((action) => action.request), + switchMap((request) => + from( + this.dropletsService.createNewFlow(request.bucket.link.href, request.name, request.description) + ).pipe( + map((res) => DropletsActions.importNewFlow({ href: res.link.href, request: request })), + catchError((errorResponse: HttpErrorResponse) => { + this.dialog.closeAll(); + return of( + ErrorActions.snackBarError({ error: this.errorHelper.getErrorString(errorResponse) }) + ); + }) + ) + ) + ) + ); + + importNewFlow$ = createEffect(() => + this.actions$.pipe( + ofType(DropletsActions.importNewFlow), + switchMap(({ href, request }) => + from(this.dropletsService.uploadFlow(href, request.file)).pipe( + map((res) => DropletsActions.importNewFlowSuccess({ response: res })), + catchError((errorResponse: HttpErrorResponse) => { + this.dialog.closeAll(); Review Comment: importNewFlowSuccess reducer has `draftState.saving = false`, in case of an error `draftState.saving` remains true. Same question for any `...Success` reducer. If there is no `...Error` reducer defined, how to recover from the loading state? ########## nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/expolorer/feature/ui/droplet-table-filter/droplet-table-filter.component.ts: ########## @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AfterViewInit, Component, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatInputModule } from '@angular/material/input'; +import { Bucket } from 'apps/nifi-registry/src/app/state/buckets'; +import { debounceTime } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatButtonModule } from '@angular/material/button'; + +export interface DropletTableFilterColumn { + key: string; + label: string; +} + +export interface DropletTableFilterArgs { + filterTerm: string; + filterColumn: string; + filterBucket?: string; +} + +export interface DropletTableFilterContext extends DropletTableFilterArgs { + changedField: string; +} + +@Component({ + selector: 'droplet-table-filter', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, MatFormFieldModule, MatSelectModule, MatInputModule, MatButtonModule], + templateUrl: './droplet-table-filter.component.html', + styleUrl: './droplet-table-filter.component.scss' +}) +export class DropletTableFilterComponent implements AfterViewInit { + filterForm: FormGroup; + private _initialFilterColumn = 'name'; + private _filterableColumns: DropletTableFilterColumn[] = []; + private _buckets: Bucket[] = []; + private _filteredCount = 0; + private _totalCount = 0; + private destroyRef: DestroyRef = inject(DestroyRef); + + set filterableColumns(filterableColumns: DropletTableFilterColumn[]) { + this._filterableColumns = filterableColumns; + } + get filterableColumns(): DropletTableFilterColumn[] { + return this._filterableColumns; + } + + @Input() set buckets(filterableColumns: Bucket[]) { + this._buckets = filterableColumns; + } + get buckets(): Bucket[] { + return this._buckets; + } + + @Input() set filterTerm(term: string) { + this.filterForm.controls['filterTerm']?.setValue(term); + } + + @Input() set filterColumn(column: string) { + this._initialFilterColumn = column; + if (this.filterableColumns?.length > 0) { + if (this.filterableColumns.findIndex((col) => col.key === column) >= 0) { + this.filterForm.get('filterColumn')?.setValue(column); + } else { + this.filterForm.get('filterColumn')?.setValue(this.filterableColumns[0].key); + } + } else { + this.filterForm.get('filterColumn')?.setValue(this._initialFilterColumn); + } + } + + @Input() set filterBucket(term: string) { + this.filterForm.controls['filterBucket']?.setValue(term); + } + + @Input() set filteredCount(filteredCount: number) { + this._filteredCount = filteredCount; + } + + get filteredCount(): number { + return this._filteredCount; + } + + @Input() set totalCount(totalCount: number) { + this._totalCount = totalCount; + } + + get totalCount(): number { + return this._totalCount; + } Review Comment: what is the purpose to use getter/setter here instead of `@Input() totalCount: number` ########## nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/pages/expolorer/feature/ui/import-new-flow-version-dialog/import-new-flow-version-dialog.component.ts: ########## @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, ElementRef, Inject, ViewChild } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { CloseOnEscapeDialog } from '@nifi/shared'; +import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { importNewFlow } from 'apps/nifi-registry/src/app/state/droplets/droplets.actions'; +import { Droplets } from 'apps/nifi-registry/src/app/state/droplets'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; + +interface Data { Review Comment: IMO it is worh it to export the interface and use in this.dialog.open<ImportNewFlowVersionDialogComponent, ImportNewFlowVersionDialogData>(ImportNewFlowVersionDialogComponent, { data // becomes type safe } ```suggestion export interface ImportNewFlowVersionDialogData { ``` ########## nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/state/droplets/droplets.effects.ts: ########## @@ -0,0 +1,280 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { inject, Injectable } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { MatDialog } from '@angular/material/dialog'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { concatLatestFrom } from '@ngrx/operators'; +import { catchError, from, map, of, switchMap, tap } from 'rxjs'; +import { MEDIUM_DIALOG, SMALL_DIALOG, XL_DIALOG } from '@nifi/shared'; +import { NiFiState } from '../index'; +import { DropletsService } from '../../service/droplets.service'; +import * as DropletsActions from './droplets.actions'; +import { DeleteDropletDialogComponent } from '../../pages/expolorer/feature/ui/delete-droplet-dialog/delete-droplet-dialog.component'; +import { ImportNewFlowDialogComponent } from '../../pages/expolorer/feature/ui/import-new-flow-dialog/import-new-flow-dialog.component'; +import { ImportNewFlowVersionDialogComponent } from '../../pages/expolorer/feature/ui/import-new-flow-version-dialog/import-new-flow-version-dialog.component'; +import { ExportFlowVersionDialogComponent } from '../../pages/expolorer/feature/ui/export-flow-version-dialog/export-flow-version-dialog.component'; +import { FlowVersionsDialogComponent } from '../../pages/expolorer/feature/ui/flow-versions-dialog/flow-versions-dialog.component'; +import { ErrorHelper } from '../../service/error-helper.service'; +import * as ErrorActions from '../../state/error/error.actions'; +import { selectStatus } from './droplets.selectors'; + +@Injectable() +export class DropletsEffects { + constructor( + private store: Store<NiFiState>, + private dropletsService: DropletsService, + private dialog: MatDialog, + private errorHelper: ErrorHelper + ) {} + + actions$ = inject(Actions); + + loadDroplets$ = createEffect(() => + this.actions$.pipe( + ofType(DropletsActions.loadDroplets), + concatLatestFrom(() => this.store.select(selectStatus)), + switchMap(([, status]) => { + return from( + this.dropletsService.getDroplets().pipe( + map((response) => + DropletsActions.loadDropletsSuccess({ + response: { + droplets: response + } + }) + ), + catchError((errorResponse: HttpErrorResponse) => + of(this.errorHelper.handleLoadingError(status, errorResponse)) + ) + ) + ); + }) + ) + ); + + openDeleteDropletDialog$ = createEffect( + () => + this.actions$.pipe( + ofType(DropletsActions.openDeleteDropletDialog), + tap(({ request }) => { + this.dialog.open(DeleteDropletDialogComponent, { + ...SMALL_DIALOG, + autoFocus: false, + data: request + }); + }) + ), + { dispatch: false } + ); + + deleteDroplet$ = createEffect(() => + this.actions$.pipe( + ofType(DropletsActions.deleteDroplet), + map((action) => action.request), + switchMap((request) => + from(this.dropletsService.deleteDroplet(request.droplet.link.href)).pipe( + map((res) => DropletsActions.deleteDropletSuccess({ response: res })), + catchError((errorResponse: HttpErrorResponse) => + of(ErrorActions.snackBarError({ error: this.errorHelper.getErrorString(errorResponse) })) + ) + ) + ) + ) + ); + + deleteDropletSuccess$ = createEffect(() => + this.actions$.pipe( + ofType(DropletsActions.deleteDropletSuccess), + switchMap(() => of(DropletsActions.loadDroplets())) + ) + ); + + openImportNewFlowDialog$ = createEffect( + () => + this.actions$.pipe( + ofType(DropletsActions.openImportNewFlowDialog), + map((action) => action.request), + tap((request) => { + this.dialog.open(ImportNewFlowDialogComponent, { + ...MEDIUM_DIALOG, + autoFocus: false, + data: { + activeBucket: request.activeBucket, + buckets: request.buckets + } + }); + }) + ), + { dispatch: false } + ); + + createNewFlow$ = createEffect(() => + this.actions$.pipe( + ofType(DropletsActions.createNewFlow), + map((action) => action.request), + switchMap((request) => + from( + this.dropletsService.createNewFlow(request.bucket.link.href, request.name, request.description) + ).pipe( + map((res) => DropletsActions.importNewFlow({ href: res.link.href, request: request })), + catchError((errorResponse: HttpErrorResponse) => { + this.dialog.closeAll(); + return of( + ErrorActions.snackBarError({ error: this.errorHelper.getErrorString(errorResponse) }) + ); + }) + ) + ) + ) + ); + + importNewFlow$ = createEffect(() => + this.actions$.pipe( + ofType(DropletsActions.importNewFlow), + switchMap(({ href, request }) => + from(this.dropletsService.uploadFlow(href, request.file)).pipe( + map((res) => DropletsActions.importNewFlowSuccess({ response: res })), + catchError((errorResponse: HttpErrorResponse) => { + this.dialog.closeAll(); + return of( + ErrorActions.snackBarError({ error: this.errorHelper.getErrorString(errorResponse) }) + ); + }) + ) + ) + ) + ); + + importNewFlowSuccess$ = createEffect(() => Review Comment: deleteDropletSuccess$ ? ########## nifi-frontend/src/main/frontend/apps/nifi-registry/src/app/ui/header/header.component.ts: ########## @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, OnInit, Signal } from '@angular/core'; +import { CommonModule, NgOptimizedImage } from '@angular/common'; +import { Store } from '@ngrx/store'; +import { selectCurrentUser } from '../../state/current-user/current-user.selectors'; +import { CurrentUser } from '../../state/current-user'; +import { loadCurrentUser, startCurrentUserPolling } from '../../state/current-user/current-user.actions'; +import { RouterModule } from '@angular/router'; + +@Component({ + selector: 'app-header', + standalone: true, + imports: [CommonModule, NgOptimizedImage, RouterModule], + templateUrl: './header.component.html', + styleUrl: './header.component.scss' +}) +export class HeaderComponent implements OnInit { + currentUser: Signal<CurrentUser>; Review Comment: is it used or planned to use next time? -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
