scottyaslan commented on code in PR #10473:
URL: https://github.com/apache/nifi/pull/10473#discussion_r2478349220
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-dialog/change-version-dialog.ts:
##########
@@ -47,16 +49,18 @@ export class ChangeVersionDialog extends
CloseOnEscapeDialog {
private nifiCommon = inject(NiFiCommon);
private store = inject<Store<CanvasState>>(Store);
- displayedColumns: string[] = ['current', 'version', 'created', 'comments'];
+ displayedColumns: string[] = ['actions', 'current', 'version', 'created',
'comments'];
Review Comment:
The actions column should be displayed as the last column in the table
listing.
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.scss:
##########
@@ -0,0 +1,94 @@
+/*!
+ * 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;
+
+.flow-diff {
+ @include mat.button-density(-1);
+
+ .flow-diff-message {
+ color: var(--mat-sys-on-surface-variant);
+
+ .flow-diff-message-title {
+ font-weight: 600;
+ margin-bottom: 4px;
+ }
+
+ .flow-diff-message-list {
+ margin: 0;
+ padding-left: 16px;
+ list-style: disc;
+
+ li {
+ margin-bottom: 2px;
+
+ .flow-diff-version {
+ font-weight: 500;
+ }
+
+ .flow-diff-created {
+ margin-left: 4px;
+ color: var(--mat-sys-on-surface-variant);
+ }
+ }
+ }
Review Comment:
Again, all typography should be handled by the design system so these should
not be needed. Similarly the li styles should already be set by the design
system. If we truly need the margins then please update the template to use
tailwind declarative styles rather than setting them here in the SASS partial.
The reason is that all of the spacing is configurable and handled by tailwiind.
This is really powerful as it allows us to update the tailwind config in a
single place and the new spacing will be applied application wide.
```suggestion
```
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.html:
##########
@@ -0,0 +1,123 @@
+<!--
+ ~ 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.
+ -->
+
+<h2 mat-dialog-title>
+ Flow Version Diff - {{ flowName }}
+</h2>
+<div class="flow-diff">
+ <mat-dialog-content>
+ <div class="dialog-content flex flex-col gap-y-4">
+ <div
+ class="flow-diff-message"
+ data-qa="flow-diff-message"
+ *ngIf="comparisonSummary.length > 0">
+ <div class="flow-diff-message-title">Comparing versions:</div>
+ <ul class="flow-diff-message-list">
Review Comment:
An unordered list is not the correct way to display this information. Please
follow the label/value UX used throughout the application to display values.
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.html:
##########
@@ -0,0 +1,123 @@
+<!--
+ ~ 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.
+ -->
+
+<h2 mat-dialog-title>
+ Flow Version Diff - {{ flowName }}
+</h2>
+<div class="flow-diff">
+ <mat-dialog-content>
+ <div class="dialog-content flex flex-col gap-y-4">
+ <div
+ class="flow-diff-message"
+ data-qa="flow-diff-message"
+ *ngIf="comparisonSummary.length > 0">
+ <div class="flow-diff-message-title">Comparing versions:</div>
+ <ul class="flow-diff-message-list">
+ <li *ngFor="let summary of comparisonSummary">
+ <span class="flow-diff-version">Version {{
summary.version }}</span>
+ <span *ngIf="summary.created"
class="flow-diff-created">
+ ({{ summary.created }})
+ </span>
+ </li>
+ </ul>
+ </div>
+
+ <div class="flow-diff-controls flex flex-wrap gap-4">
+ <mat-form-field class="flow-diff-select" appearance="outline">
+ <mat-label>Current Version</mat-label>
+ <mat-select [formControl]="currentVersionControl"
data-qa="current-version-select">
+ <mat-option *ngFor="let version of versionOptions"
[value]="version">
+ {{ formatVersionOption(version) }}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+
+ <mat-form-field class="flow-diff-select" appearance="outline">
+ <mat-label>Selected Version</mat-label>
+ <mat-select [formControl]="selectedVersionControl"
data-qa="selected-version-select">
+ <mat-option *ngFor="let version of versionOptions"
[value]="version">
+ {{ formatVersionOption(version) }}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+
+ <mat-form-field class="flow-diff-filter" appearance="outline">
+ <mat-label>Filter</mat-label>
+ <input
+ matInput
+ type="text"
+ [formControl]="filterControl"
+ placeholder="Filter differences"
+ data-qa="flow-diff-filter" />
+ </mat-form-field>
+ </div>
+
+ <div class="flow-diff-table flex-1">
+ <div class="flow-diff-loading" *ngIf="isLoading">
+ <mat-progress-spinner diameter="36"
mode="indeterminate"></mat-progress-spinner>
+ </div>
Review Comment:
The listings in nifi do not show mat-progress-spinner or any other loading
UX.
The listings in n
```suggestion
```
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-dialog/change-version-dialog.scss:
##########
@@ -22,6 +22,10 @@
.listing-table {
table {
+ .mat-column-actions {
+ width: 54px;
+ }
+
Review Comment:
actions column is already set for all listings so we don't need this here
```suggestion
```
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/change-version-dialog/change-version-dialog.html:
##########
@@ -78,6 +78,24 @@ <h2 mat-dialog-title>
(matSortChange)="sortData($event)"
[matSortActive]="sort.active"
[matSortDirection]="sort.direction">
+ <!-- Actions Column -->
Review Comment:
The actions column should be displayed as the last column in the table
listing.
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.html:
##########
@@ -0,0 +1,123 @@
+<!--
+ ~ 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.
+ -->
+
+<h2 mat-dialog-title>
+ Flow Version Diff - {{ flowName }}
+</h2>
+<div class="flow-diff">
+ <mat-dialog-content>
+ <div class="dialog-content flex flex-col gap-y-4">
+ <div
+ class="flow-diff-message"
+ data-qa="flow-diff-message"
+ *ngIf="comparisonSummary.length > 0">
+ <div class="flow-diff-message-title">Comparing versions:</div>
+ <ul class="flow-diff-message-list">
+ <li *ngFor="let summary of comparisonSummary">
+ <span class="flow-diff-version">Version {{
summary.version }}</span>
+ <span *ngIf="summary.created"
class="flow-diff-created">
+ ({{ summary.created }})
+ </span>
+ </li>
+ </ul>
+ </div>
+
+ <div class="flow-diff-controls flex flex-wrap gap-4">
+ <mat-form-field class="flow-diff-select" appearance="outline">
+ <mat-label>Current Version</mat-label>
+ <mat-select [formControl]="currentVersionControl"
data-qa="current-version-select">
+ <mat-option *ngFor="let version of versionOptions"
[value]="version">
+ {{ formatVersionOption(version) }}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+
+ <mat-form-field class="flow-diff-select" appearance="outline">
+ <mat-label>Selected Version</mat-label>
+ <mat-select [formControl]="selectedVersionControl"
data-qa="selected-version-select">
+ <mat-option *ngFor="let version of versionOptions"
[value]="version">
+ {{ formatVersionOption(version) }}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+
+ <mat-form-field class="flow-diff-filter" appearance="outline">
+ <mat-label>Filter</mat-label>
+ <input
+ matInput
+ type="text"
+ [formControl]="filterControl"
+ placeholder="Filter differences"
+ data-qa="flow-diff-filter" />
+ </mat-form-field>
+ </div>
+
+ <div class="flow-diff-table flex-1">
+ <div class="flow-diff-loading" *ngIf="isLoading">
+ <mat-progress-spinner diameter="36"
mode="indeterminate"></mat-progress-spinner>
+ </div>
+
+ <div *ngIf="!isLoading">
+ <div *ngIf="errorMessage" class="flow-diff-error"
data-qa="flow-diff-error">
+ {{ errorMessage }}
+ </div>
+
+ <ng-container *ngIf="!errorMessage">
+ <div *ngIf="noDifferences" class="flow-diff-empty"
data-qa="flow-diff-empty">
+ No differences to display.
+ </div>
+
+ <table
Review Comment:
Ths styles for this table do not match the style for other listings
throughout the app.
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.scss:
##########
@@ -0,0 +1,94 @@
+/*!
+ * 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;
+
+.flow-diff {
+ @include mat.button-density(-1);
+
+ .flow-diff-message {
+ color: var(--mat-sys-on-surface-variant);
Review Comment:
I am curious as to why this is needed? Typography should already be properly
styled.
```suggestion
```
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.scss:
##########
@@ -0,0 +1,94 @@
+/*!
+ * 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;
+
+.flow-diff {
+ @include mat.button-density(-1);
+
+ .flow-diff-message {
+ color: var(--mat-sys-on-surface-variant);
+
+ .flow-diff-message-title {
+ font-weight: 600;
+ margin-bottom: 4px;
+ }
Review Comment:
These styles are set as part of the design system and should not be needed
here.
```suggestion
```
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.scss:
##########
@@ -0,0 +1,94 @@
+/*!
+ * 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;
+
+.flow-diff {
+ @include mat.button-density(-1);
+
+ .flow-diff-message {
+ color: var(--mat-sys-on-surface-variant);
+
+ .flow-diff-message-title {
+ font-weight: 600;
+ margin-bottom: 4px;
+ }
+
+ .flow-diff-message-list {
+ margin: 0;
+ padding-left: 16px;
+ list-style: disc;
+
+ li {
+ margin-bottom: 2px;
+
+ .flow-diff-version {
+ font-weight: 500;
+ }
+
+ .flow-diff-created {
+ margin-left: 4px;
+ color: var(--mat-sys-on-surface-variant);
+ }
+ }
+ }
+ }
+
+ .flow-diff-controls {
+ .mat-mdc-form-field {
+ min-width: 200px;
+ }
+
+ .flow-diff-filter {
+ flex: 1 1 240px;
+ }
+ }
+
+ .flow-diff-table {
+ min-height: 240px;
+
+ table {
+ width: 100%;
+
+ .mat-column-componentName,
+ .mat-column-changeType {
+ width: 200px;
+ }
+
+ .mat-column-difference {
+ width: auto;
+ }
+ }
+
+ .flow-diff-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 200px;
+ }
+
+ .flow-diff-empty,
+ .flow-diff-error {
+ padding: 12px 0;
+ color: var(--mat-sys-on-surface-variant);
+ }
+
+ .flow-diff-error {
+ color: var(--mat-sys-error);
+ }
+ }
Review Comment:
Please use tailwind declarative styles to apply these rules in the template
if they are actually needed but I don't think they are.
```suggestion
```
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.ts:
##########
@@ -0,0 +1,298 @@
+/*
+ * 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,
+ ViewChild,
+ inject
+} from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { MatSort, MatSortModule } from '@angular/material/sort';
+import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from
'@angular/forms';
+import { CommonModule } from '@angular/common';
+import { MatFormField, MatLabel } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatOption } from '@angular/material/core';
+import { MatInput } from '@angular/material/input';
+import { MatButton } from '@angular/material/button';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
+import { combineLatest, of } from 'rxjs';
+import { catchError, debounceTime, distinctUntilChanged, filter, map,
startWith, switchMap, take } from 'rxjs/operators';
+import { FlowComparisonEntity, VersionControlInformation } from
'../../../../../state/flow';
+import { VersionedFlowSnapshotMetadata } from
'../../../../../../../state/shared';
+import { RegistryService } from '../../../../../service/registry.service';
+import { CloseOnEscapeDialog, NiFiCommon } from '@nifi/shared';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { Store } from '@ngrx/store';
+import { CanvasState } from '../../../../../state';
+import { selectTimeOffset } from
'../../../../../../../state/flow-configuration/flow-configuration.selectors';
+
+export interface FlowDiffDialogData {
+ versionControlInformation: VersionControlInformation;
+ versions: VersionedFlowSnapshotMetadata[];
+ currentVersion: string;
+ selectedVersion: string;
+}
+
+interface FlowDiffRow {
+ componentName: string;
+ changeType: string;
+ difference: string;
+}
+
+@Component({
+ selector: 'flow-diff-dialog',
+ standalone: true,
+ imports: [
+ CommonModule,
+ MatDialogModule,
+ MatTableModule,
+ MatSortModule,
+ ReactiveFormsModule,
+ MatFormField,
+ MatLabel,
+ MatSelectModule,
+ MatOption,
+ MatInput,
+ MatButton,
+ MatProgressSpinner
+ ],
+ templateUrl: './flow-diff-dialog.html',
+ styleUrl: './flow-diff-dialog.scss'
+})
+export class FlowDiffDialog extends CloseOnEscapeDialog implements
AfterViewInit {
+ private data = inject<FlowDiffDialogData>(MAT_DIALOG_DATA);
+ private registryService = inject(RegistryService);
+ private destroyRef = inject(DestroyRef);
+ private formBuilder = inject(FormBuilder);
+ private nifiCommon = inject(NiFiCommon);
+ private store = inject<Store<CanvasState>>(Store);
+ private timeOffset = this.store.selectSignal(selectTimeOffset);
+
+ @ViewChild(MatSort)
+ set matSort(sort: MatSort | undefined) {
+ if (sort) {
+ this.dataSource.sort = sort;
+ }
+ }
+
+ displayedColumns: string[] = ['componentName', 'changeType', 'difference'];
+ dataSource: MatTableDataSource<FlowDiffRow> = new
MatTableDataSource<FlowDiffRow>();
+ comparisonForm: FormGroup;
+ filterControl: FormControl<string> = new FormControl<string>('', {
nonNullable: true });
+ currentVersionControl: FormControl<string>;
+ selectedVersionControl: FormControl<string>;
+
+ versionOptions: string[];
+ flowName: string;
+ comparisonSummary: { version: string; created?: string }[] = [];
+ isLoading = false;
+ errorMessage: string | null = null;
+ noDifferences = false;
+ private versionMetadataByVersion: Map<string,
VersionedFlowSnapshotMetadata> = new Map();
+
+ constructor() {
+ super();
+ const versions = this.sortVersions(this.data.versions);
+ this.versionOptions = versions.map((version) => version.version);
+ this.versionMetadataByVersion = new Map(versions.map((metadata) =>
[metadata.version, metadata]));
+ const vci = this.data.versionControlInformation;
+ this.flowName = vci.flowName || vci.flowId;
+
+ this.currentVersionControl = new
FormControl<string>(this.data.currentVersion, { nonNullable: true });
+ this.selectedVersionControl = new
FormControl<string>(this.data.selectedVersion, { nonNullable: true });
+ this.comparisonForm = this.formBuilder.group({
+ currentVersion: this.currentVersionControl,
+ selectedVersion: this.selectedVersionControl
+ });
+
+ this.dataSource.filterPredicate = (row: FlowDiffRow, filterTerm:
string) => {
+ if (!filterTerm) {
+ return true;
+ }
+
+ const normalizedFilter = filterTerm.toLowerCase();
+ return (
+ (row.componentName ||
'').toLowerCase().includes(normalizedFilter) ||
+ (row.changeType ||
'').toLowerCase().includes(normalizedFilter) ||
+ (row.difference || '').toLowerCase().includes(normalizedFilter)
+ );
+ };
+
+ this.dataSource.sortingDataAccessor = (row: FlowDiffRow, property:
string) => {
+ switch (property) {
+ case 'componentName':
+ return (row.componentName || '').toLowerCase();
+ case 'changeType':
+ return (row.changeType || '').toLowerCase();
+ case 'difference':
+ return (row.difference || '').toLowerCase();
+ default:
+ return '';
+ }
+ };
+
+ this.configureFiltering();
+ this.configureComparisonChanges();
+ this.setComparisonSummary(this.data.currentVersion,
this.data.selectedVersion);
+ }
+
+ formatVersionOption(version: string): string {
+ const metadata = this.versionMetadataByVersion.get(version);
+ const formattedVersion =
+ version.length > 5 ? `${version.substring(0, 5)}...` : version;
+ const created = metadata ? this.formatTimestamp(metadata) : undefined;
+ return created ? `${formattedVersion} (${created})` : formattedVersion;
+ }
+
+ ngAfterViewInit(): void {
+ // Sorting handled automatically when MatSort is available via the
ViewChild setter
Review Comment:
I haven't really dug in here yet but it seems sorting is handled differently
for this listing then other listings throughout nifi. I am not saying this is
incorrect but it is a different pattern so at the very least it would need some
testing and verification. It would be nice it we could just update this to
follow the pattern used in other listings. We should not need to use a
ViewChild to sort an angular material table.
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.scss:
##########
@@ -0,0 +1,94 @@
+/*!
+ * 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;
+
+.flow-diff {
+ @include mat.button-density(-1);
+
+ .flow-diff-message {
+ color: var(--mat-sys-on-surface-variant);
+
+ .flow-diff-message-title {
+ font-weight: 600;
+ margin-bottom: 4px;
+ }
+
+ .flow-diff-message-list {
+ margin: 0;
+ padding-left: 16px;
+ list-style: disc;
+
+ li {
+ margin-bottom: 2px;
+
+ .flow-diff-version {
+ font-weight: 500;
+ }
+
+ .flow-diff-created {
+ margin-left: 4px;
+ color: var(--mat-sys-on-surface-variant);
+ }
+ }
+ }
+ }
+
+ .flow-diff-controls {
+ .mat-mdc-form-field {
+ min-width: 200px;
+ }
+
+ .flow-diff-filter {
+ flex: 1 1 240px;
+ }
Review Comment:
Please use tailwind declaritive styles to apply these rules in the template.
```suggestion
```
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.html:
##########
@@ -0,0 +1,123 @@
+<!--
+ ~ 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.
+ -->
+
+<h2 mat-dialog-title>
+ Flow Version Diff - {{ flowName }}
+</h2>
+<div class="flow-diff">
+ <mat-dialog-content>
+ <div class="dialog-content flex flex-col gap-y-4">
+ <div
+ class="flow-diff-message"
+ data-qa="flow-diff-message"
+ *ngIf="comparisonSummary.length > 0">
+ <div class="flow-diff-message-title">Comparing versions:</div>
+ <ul class="flow-diff-message-list">
+ <li *ngFor="let summary of comparisonSummary">
+ <span class="flow-diff-version">Version {{
summary.version }}</span>
+ <span *ngIf="summary.created"
class="flow-diff-created">
+ ({{ summary.created }})
+ </span>
+ </li>
+ </ul>
+ </div>
+
+ <div class="flow-diff-controls flex flex-wrap gap-4">
+ <mat-form-field class="flow-diff-select" appearance="outline">
+ <mat-label>Current Version</mat-label>
+ <mat-select [formControl]="currentVersionControl"
data-qa="current-version-select">
+ <mat-option *ngFor="let version of versionOptions"
[value]="version">
+ {{ formatVersionOption(version) }}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+
+ <mat-form-field class="flow-diff-select" appearance="outline">
+ <mat-label>Selected Version</mat-label>
+ <mat-select [formControl]="selectedVersionControl"
data-qa="selected-version-select">
+ <mat-option *ngFor="let version of versionOptions"
[value]="version">
+ {{ formatVersionOption(version) }}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+
+ <mat-form-field class="flow-diff-filter" appearance="outline">
+ <mat-label>Filter</mat-label>
+ <input
+ matInput
+ type="text"
+ [formControl]="filterControl"
+ placeholder="Filter differences"
+ data-qa="flow-diff-filter" />
+ </mat-form-field>
+ </div>
+
+ <div class="flow-diff-table flex-1">
+ <div class="flow-diff-loading" *ngIf="isLoading">
+ <mat-progress-spinner diameter="36"
mode="indeterminate"></mat-progress-spinner>
+ </div>
+
+ <div *ngIf="!isLoading">
+ <div *ngIf="errorMessage" class="flow-diff-error"
data-qa="flow-diff-error">
+ {{ errorMessage }}
+ </div>
Review Comment:
The listings in nifi do not show errors like this. If there is an error when
requesting the diff it should be displayed in a context error banner at the top
of the dialog like we do in other cases.
```suggestion
```
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.scss:
##########
@@ -0,0 +1,94 @@
+/*!
+ * 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;
+
+.flow-diff {
+ @include mat.button-density(-1);
+
+ .flow-diff-message {
+ color: var(--mat-sys-on-surface-variant);
+
+ .flow-diff-message-title {
+ font-weight: 600;
+ margin-bottom: 4px;
+ }
+
+ .flow-diff-message-list {
+ margin: 0;
+ padding-left: 16px;
+ list-style: disc;
+
+ li {
+ margin-bottom: 2px;
+
+ .flow-diff-version {
+ font-weight: 500;
+ }
+
+ .flow-diff-created {
+ margin-left: 4px;
+ color: var(--mat-sys-on-surface-variant);
+ }
+ }
+ }
+ }
+
+ .flow-diff-controls {
+ .mat-mdc-form-field {
+ min-width: 200px;
+ }
Review Comment:
This does not seem correct. Form fields should all be correctly styles by
the design system and should not need width set.
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.html:
##########
@@ -0,0 +1,123 @@
+<!--
+ ~ 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.
+ -->
+
+<h2 mat-dialog-title>
+ Flow Version Diff - {{ flowName }}
+</h2>
+<div class="flow-diff">
+ <mat-dialog-content>
+ <div class="dialog-content flex flex-col gap-y-4">
+ <div
+ class="flow-diff-message"
+ data-qa="flow-diff-message"
+ *ngIf="comparisonSummary.length > 0">
+ <div class="flow-diff-message-title">Comparing versions:</div>
+ <ul class="flow-diff-message-list">
+ <li *ngFor="let summary of comparisonSummary">
+ <span class="flow-diff-version">Version {{
summary.version }}</span>
+ <span *ngIf="summary.created"
class="flow-diff-created">
+ ({{ summary.created }})
+ </span>
+ </li>
+ </ul>
+ </div>
+
+ <div class="flow-diff-controls flex flex-wrap gap-4">
+ <mat-form-field class="flow-diff-select" appearance="outline">
+ <mat-label>Current Version</mat-label>
+ <mat-select [formControl]="currentVersionControl"
data-qa="current-version-select">
+ <mat-option *ngFor="let version of versionOptions"
[value]="version">
+ {{ formatVersionOption(version) }}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+
+ <mat-form-field class="flow-diff-select" appearance="outline">
+ <mat-label>Selected Version</mat-label>
+ <mat-select [formControl]="selectedVersionControl"
data-qa="selected-version-select">
+ <mat-option *ngFor="let version of versionOptions"
[value]="version">
+ {{ formatVersionOption(version) }}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+
+ <mat-form-field class="flow-diff-filter" appearance="outline">
+ <mat-label>Filter</mat-label>
+ <input
+ matInput
+ type="text"
+ [formControl]="filterControl"
+ placeholder="Filter differences"
+ data-qa="flow-diff-filter" />
+ </mat-form-field>
+ </div>
+
+ <div class="flow-diff-table flex-1">
+ <div class="flow-diff-loading" *ngIf="isLoading">
+ <mat-progress-spinner diameter="36"
mode="indeterminate"></mat-progress-spinner>
+ </div>
+
+ <div *ngIf="!isLoading">
+ <div *ngIf="errorMessage" class="flow-diff-error"
data-qa="flow-diff-error">
+ {{ errorMessage }}
+ </div>
+
+ <ng-container *ngIf="!errorMessage">
+ <div *ngIf="noDifferences" class="flow-diff-empty"
data-qa="flow-diff-empty">
+ No differences to display.
+ </div>
+
+ <table
+ mat-table
+ *ngIf="dataSource.data.length > 0"
Review Comment:
```suggestion
```
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.ts:
##########
@@ -0,0 +1,298 @@
+/*
+ * 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,
+ ViewChild,
+ inject
+} from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { MatSort, MatSortModule } from '@angular/material/sort';
+import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from
'@angular/forms';
+import { CommonModule } from '@angular/common';
+import { MatFormField, MatLabel } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatOption } from '@angular/material/core';
+import { MatInput } from '@angular/material/input';
+import { MatButton } from '@angular/material/button';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
+import { combineLatest, of } from 'rxjs';
+import { catchError, debounceTime, distinctUntilChanged, filter, map,
startWith, switchMap, take } from 'rxjs/operators';
+import { FlowComparisonEntity, VersionControlInformation } from
'../../../../../state/flow';
+import { VersionedFlowSnapshotMetadata } from
'../../../../../../../state/shared';
+import { RegistryService } from '../../../../../service/registry.service';
+import { CloseOnEscapeDialog, NiFiCommon } from '@nifi/shared';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { Store } from '@ngrx/store';
+import { CanvasState } from '../../../../../state';
+import { selectTimeOffset } from
'../../../../../../../state/flow-configuration/flow-configuration.selectors';
+
+export interface FlowDiffDialogData {
+ versionControlInformation: VersionControlInformation;
+ versions: VersionedFlowSnapshotMetadata[];
+ currentVersion: string;
+ selectedVersion: string;
+}
+
+interface FlowDiffRow {
+ componentName: string;
+ changeType: string;
+ difference: string;
+}
+
+@Component({
+ selector: 'flow-diff-dialog',
+ standalone: true,
+ imports: [
+ CommonModule,
+ MatDialogModule,
+ MatTableModule,
+ MatSortModule,
+ ReactiveFormsModule,
+ MatFormField,
+ MatLabel,
+ MatSelectModule,
+ MatOption,
+ MatInput,
+ MatButton,
+ MatProgressSpinner
+ ],
+ templateUrl: './flow-diff-dialog.html',
+ styleUrl: './flow-diff-dialog.scss'
+})
+export class FlowDiffDialog extends CloseOnEscapeDialog implements
AfterViewInit {
+ private data = inject<FlowDiffDialogData>(MAT_DIALOG_DATA);
+ private registryService = inject(RegistryService);
+ private destroyRef = inject(DestroyRef);
+ private formBuilder = inject(FormBuilder);
+ private nifiCommon = inject(NiFiCommon);
+ private store = inject<Store<CanvasState>>(Store);
+ private timeOffset = this.store.selectSignal(selectTimeOffset);
+
+ @ViewChild(MatSort)
Review Comment:
You can follow how the change version dialog implemented sorting instead of
using a ViewChild here.
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.ts:
##########
@@ -0,0 +1,298 @@
+/*
+ * 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,
+ ViewChild,
+ inject
+} from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { MatSort, MatSortModule } from '@angular/material/sort';
+import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from
'@angular/forms';
+import { CommonModule } from '@angular/common';
+import { MatFormField, MatLabel } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatOption } from '@angular/material/core';
+import { MatInput } from '@angular/material/input';
+import { MatButton } from '@angular/material/button';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
+import { combineLatest, of } from 'rxjs';
+import { catchError, debounceTime, distinctUntilChanged, filter, map,
startWith, switchMap, take } from 'rxjs/operators';
+import { FlowComparisonEntity, VersionControlInformation } from
'../../../../../state/flow';
+import { VersionedFlowSnapshotMetadata } from
'../../../../../../../state/shared';
+import { RegistryService } from '../../../../../service/registry.service';
+import { CloseOnEscapeDialog, NiFiCommon } from '@nifi/shared';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { Store } from '@ngrx/store';
+import { CanvasState } from '../../../../../state';
+import { selectTimeOffset } from
'../../../../../../../state/flow-configuration/flow-configuration.selectors';
+
+export interface FlowDiffDialogData {
+ versionControlInformation: VersionControlInformation;
+ versions: VersionedFlowSnapshotMetadata[];
+ currentVersion: string;
+ selectedVersion: string;
+}
+
+interface FlowDiffRow {
+ componentName: string;
+ changeType: string;
+ difference: string;
+}
+
+@Component({
+ selector: 'flow-diff-dialog',
+ standalone: true,
+ imports: [
+ CommonModule,
+ MatDialogModule,
+ MatTableModule,
+ MatSortModule,
+ ReactiveFormsModule,
+ MatFormField,
+ MatLabel,
+ MatSelectModule,
+ MatOption,
+ MatInput,
+ MatButton,
+ MatProgressSpinner
+ ],
+ templateUrl: './flow-diff-dialog.html',
+ styleUrl: './flow-diff-dialog.scss'
+})
+export class FlowDiffDialog extends CloseOnEscapeDialog implements
AfterViewInit {
+ private data = inject<FlowDiffDialogData>(MAT_DIALOG_DATA);
+ private registryService = inject(RegistryService);
+ private destroyRef = inject(DestroyRef);
+ private formBuilder = inject(FormBuilder);
+ private nifiCommon = inject(NiFiCommon);
+ private store = inject<Store<CanvasState>>(Store);
Review Comment:
We should not be injecting the store in a dialog. Data should be passed into
the dialog when opened via MAT_DIALOG_DATA.
```suggestion
```
##########
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/flow/flow-diff-dialog/flow-diff-dialog.ts:
##########
@@ -0,0 +1,298 @@
+/*
+ * 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,
+ ViewChild,
+ inject
+} from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { MatSort, MatSortModule } from '@angular/material/sort';
+import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from
'@angular/forms';
+import { CommonModule } from '@angular/common';
+import { MatFormField, MatLabel } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatOption } from '@angular/material/core';
+import { MatInput } from '@angular/material/input';
+import { MatButton } from '@angular/material/button';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
+import { combineLatest, of } from 'rxjs';
+import { catchError, debounceTime, distinctUntilChanged, filter, map,
startWith, switchMap, take } from 'rxjs/operators';
+import { FlowComparisonEntity, VersionControlInformation } from
'../../../../../state/flow';
+import { VersionedFlowSnapshotMetadata } from
'../../../../../../../state/shared';
+import { RegistryService } from '../../../../../service/registry.service';
+import { CloseOnEscapeDialog, NiFiCommon } from '@nifi/shared';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { Store } from '@ngrx/store';
+import { CanvasState } from '../../../../../state';
+import { selectTimeOffset } from
'../../../../../../../state/flow-configuration/flow-configuration.selectors';
+
+export interface FlowDiffDialogData {
+ versionControlInformation: VersionControlInformation;
+ versions: VersionedFlowSnapshotMetadata[];
+ currentVersion: string;
+ selectedVersion: string;
+}
+
+interface FlowDiffRow {
+ componentName: string;
+ changeType: string;
+ difference: string;
+}
+
+@Component({
+ selector: 'flow-diff-dialog',
+ standalone: true,
+ imports: [
+ CommonModule,
+ MatDialogModule,
+ MatTableModule,
+ MatSortModule,
+ ReactiveFormsModule,
+ MatFormField,
+ MatLabel,
+ MatSelectModule,
+ MatOption,
+ MatInput,
+ MatButton,
+ MatProgressSpinner
+ ],
+ templateUrl: './flow-diff-dialog.html',
+ styleUrl: './flow-diff-dialog.scss'
+})
+export class FlowDiffDialog extends CloseOnEscapeDialog implements
AfterViewInit {
+ private data = inject<FlowDiffDialogData>(MAT_DIALOG_DATA);
+ private registryService = inject(RegistryService);
+ private destroyRef = inject(DestroyRef);
+ private formBuilder = inject(FormBuilder);
+ private nifiCommon = inject(NiFiCommon);
+ private store = inject<Store<CanvasState>>(Store);
+ private timeOffset = this.store.selectSignal(selectTimeOffset);
+
+ @ViewChild(MatSort)
+ set matSort(sort: MatSort | undefined) {
+ if (sort) {
+ this.dataSource.sort = sort;
+ }
+ }
+
+ displayedColumns: string[] = ['componentName', 'changeType', 'difference'];
+ dataSource: MatTableDataSource<FlowDiffRow> = new
MatTableDataSource<FlowDiffRow>();
+ comparisonForm: FormGroup;
+ filterControl: FormControl<string> = new FormControl<string>('', {
nonNullable: true });
+ currentVersionControl: FormControl<string>;
+ selectedVersionControl: FormControl<string>;
+
+ versionOptions: string[];
+ flowName: string;
+ comparisonSummary: { version: string; created?: string }[] = [];
+ isLoading = false;
+ errorMessage: string | null = null;
+ noDifferences = false;
+ private versionMetadataByVersion: Map<string,
VersionedFlowSnapshotMetadata> = new Map();
+
+ constructor() {
+ super();
+ const versions = this.sortVersions(this.data.versions);
+ this.versionOptions = versions.map((version) => version.version);
+ this.versionMetadataByVersion = new Map(versions.map((metadata) =>
[metadata.version, metadata]));
+ const vci = this.data.versionControlInformation;
+ this.flowName = vci.flowName || vci.flowId;
+
+ this.currentVersionControl = new
FormControl<string>(this.data.currentVersion, { nonNullable: true });
+ this.selectedVersionControl = new
FormControl<string>(this.data.selectedVersion, { nonNullable: true });
+ this.comparisonForm = this.formBuilder.group({
+ currentVersion: this.currentVersionControl,
+ selectedVersion: this.selectedVersionControl
+ });
+
+ this.dataSource.filterPredicate = (row: FlowDiffRow, filterTerm:
string) => {
+ if (!filterTerm) {
+ return true;
+ }
+
+ const normalizedFilter = filterTerm.toLowerCase();
+ return (
+ (row.componentName ||
'').toLowerCase().includes(normalizedFilter) ||
+ (row.changeType ||
'').toLowerCase().includes(normalizedFilter) ||
+ (row.difference || '').toLowerCase().includes(normalizedFilter)
+ );
+ };
+
+ this.dataSource.sortingDataAccessor = (row: FlowDiffRow, property:
string) => {
+ switch (property) {
+ case 'componentName':
+ return (row.componentName || '').toLowerCase();
+ case 'changeType':
+ return (row.changeType || '').toLowerCase();
+ case 'difference':
+ return (row.difference || '').toLowerCase();
+ default:
+ return '';
+ }
+ };
+
+ this.configureFiltering();
+ this.configureComparisonChanges();
+ this.setComparisonSummary(this.data.currentVersion,
this.data.selectedVersion);
+ }
+
+ formatVersionOption(version: string): string {
+ const metadata = this.versionMetadataByVersion.get(version);
+ const formattedVersion =
+ version.length > 5 ? `${version.substring(0, 5)}...` : version;
+ const created = metadata ? this.formatTimestamp(metadata) : undefined;
+ return created ? `${formattedVersion} (${created})` : formattedVersion;
+ }
+
+ ngAfterViewInit(): void {
+ // Sorting handled automatically when MatSort is available via the
ViewChild setter
+ }
+
+ private configureFiltering(): void {
+ this.filterControl.valueChanges
+ .pipe(
+ startWith(this.filterControl.value),
+ debounceTime(200),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe((value) => {
+ const filterTerm = value ? value.trim() : '';
+ this.dataSource.filter = filterTerm.toLowerCase();
+ });
+ }
+
+ private configureComparisonChanges(): void {
+ const currentVersion$ =
this.currentVersionControl.valueChanges.pipe(startWith(this.currentVersionControl.value));
+ const selectedVersion$ = this.selectedVersionControl.valueChanges.pipe(
+ startWith(this.selectedVersionControl.value)
+ );
+
+ combineLatest([currentVersion$, selectedVersion$])
+ .pipe(
+ takeUntilDestroyed(this.destroyRef),
+ map(([current, selected]) => [current, selected] as [string |
null, string | null]),
+ filter(([current, selected]) => !!current && !!selected),
+ distinctUntilChanged(
+ ([currentA, selectedA], [currentB, selectedB]) =>
+ currentA === currentB && selectedA === selectedB
+ ),
+ switchMap(([current, selected]) => {
+ this.isLoading = true;
+ this.errorMessage = null;
+ this.noDifferences = false;
+ return this.fetchFlowDiff(current as string, selected as
string).pipe(
+ catchError((error) => {
+ this.isLoading = false;
+ this.errorMessage = 'Unable to retrieve version
differences.';
+ console.error('Failed to load flow version diff',
error);
+ this.dataSource.data = [];
+ this.noDifferences = true;
+ return of(null);
+ })
+ );
+ })
+ )
+ .subscribe((comparison) => {
+ if (!comparison) {
+ return;
+ }
+
+ this.isLoading = false;
+ this.setComparisonSummary(this.currentVersionControl.value,
this.selectedVersionControl.value);
+ const rows = this.toRows(comparison);
+ this.dataSource.data = rows;
+ this.noDifferences = rows.length === 0;
+ });
+ }
+
+ private fetchFlowDiff(versionA: string, versionB: string) {
+ this.setComparisonSummary(versionA, versionB);
+ const vci = this.data.versionControlInformation;
+ const branch = vci.branch ?? null;
+
+ return this.registryService
+ .getFlowDiff(vci.registryId, vci.bucketId, vci.flowId, versionA,
versionB, branch)
+ .pipe(take(1));
+ }
+
+ private sortVersions(versions: VersionedFlowSnapshotMetadata[]):
VersionedFlowSnapshotMetadata[] {
+ return versions
+ .slice()
+ .sort((a, b) => {
+ const timestampA =
this.nifiCommon.isDefinedAndNotNull(a.timestamp) ? a.timestamp : 0;
+ const timestampB =
this.nifiCommon.isDefinedAndNotNull(b.timestamp) ? b.timestamp : 0;
+ const timestampComparison =
this.nifiCommon.compareNumber(timestampB, timestampA);
+ if (timestampComparison !== 0) {
+ return timestampComparison;
+ }
+
+ if (this.nifiCommon.isNumber(a.version) &&
this.nifiCommon.isNumber(b.version)) {
+ return this.nifiCommon.compareNumber(parseInt(b.version,
10), parseInt(a.version, 10));
+ }
+
+ return this.nifiCommon.compareString(b.version, a.version);
+ });
+ }
+
+ private toRows(comparison: FlowComparisonEntity): FlowDiffRow[] {
+ if (!comparison || !comparison.componentDifferences) {
+ return [];
+ }
+
+ const rows: FlowDiffRow[] = [];
+ comparison.componentDifferences.forEach((component) => {
+ component.differences.forEach((difference) => {
+ rows.push({
+ componentName: component.componentName || '',
+ changeType: difference.differenceType,
+ difference: difference.difference
+ });
+ });
+ });
+
+ return rows;
+ }
+
+ private setComparisonSummary(versionA: string, versionB: string): void {
+ this.comparisonSummary = [versionA, versionB].map((version) => {
+ const metadata = this.versionMetadataByVersion.get(version);
+ return {
+ version,
+ created: metadata ? this.formatTimestamp(metadata) : undefined
+ };
+ });
+ }
+
+ private formatTimestamp(metadata: VersionedFlowSnapshotMetadata): string |
undefined {
+ if (!metadata.timestamp) {
+ return undefined;
+ }
+
+ const now = new Date();
+ const userOffset = now.getTimezoneOffset() * 60 * 1000;
+ const date = new Date(metadata.timestamp + userOffset +
this.getTimezoneOffset());
+ return this.nifiCommon.formatDateTime(date);
+ }
+
+ private getTimezoneOffset(): number {
+ return this.timeOffset() || 0;
+ }
Review Comment:
I don't think we typically format timestamps like this. Is there a reason to
here? I now see that we do have two implementations of:
```
formatTimestamp(flowVersion: VersionedFlowSnapshotMetadata) {
// get the current user time to properly convert the server time
const now: Date = new Date();
// convert the user offset to millis
const userTimeOffset: number = now.getTimezoneOffset() * 60 * 1000;
// create the proper date by adjusting by the offsets
const date: Date = new Date(flowVersion.timestamp + userTimeOffset +
this.timeOffset);
return this.nifiCommon.formatDateTime(date);
}
```
One in the select-flow-version.componet.ts and one in the
import-from-registry.component.ts. If we really have a need for this formatting
we should have a single implementation in a shared area. A new pipe in
https://github.com/apache/nifi/tree/main/nifi-frontend/src/main/frontend/libs/shared/src/pipes
would work well for this use case.
--
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]