You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by rf...@apache.org on 2023/12/11 22:36:48 UTC
(nifi) branch main updated: NIFI-12445: Provenance Event Listing (#8133)
This is an automated email from the ASF dual-hosted git repository.
rfellows 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 4f59f46ce4 NIFI-12445: Provenance Event Listing (#8133)
4f59f46ce4 is described below
commit 4f59f46ce4d95b23805643b8f6026b0cfc4ee59a
Author: Matt Gilman <ma...@gmail.com>
AuthorDate: Mon Dec 11 17:36:42 2023 -0500
NIFI-12445: Provenance Event Listing (#8133)
- Provenance Event Listing.
- View Provenance Event dialog.
- Provenance routing.
- Provenance search.
- Replay.
- Download content.
- View content.
- Addressing review feedback.
- Addressing review feedback.
This closes #8133
---
.../src/main/nifi/proxy.config.mjs | 19 +
.../src/main/nifi/src/app/app-routing.module.ts | 5 +
.../src/main/nifi/src/app/app.module.ts | 9 +-
.../behavior/connectable-behavior.service.spec.ts | 6 +
.../behavior/draggable-behavior.service.spec.ts | 6 +
.../behavior/quick-select-behavior.service.spec.ts | 6 +
.../service/birdseye-view.service.spec.ts | 6 +
.../flow-designer/service/canvas-utils.service.ts | 60 ++-
.../service/canvas-view.service.spec.ts | 6 +
.../pages/flow-designer/service/flow.service.ts | 9 +-
.../manager/connection-manager.service.spec.ts | 6 +
.../service/manager/funnel-manager.service.spec.ts | 6 +
.../remote-process-group-manager.service.spec.ts | 6 +
.../pages/flow-designer/state/flow/flow.actions.ts | 13 +-
.../pages/flow-designer/state/flow/flow.effects.ts | 88 ++++
.../app/pages/flow-designer/state/flow/index.ts | 5 +
.../canvas/context-menu/context-menu.component.ts | 47 ++-
.../ui/canvas/header/header.component.html | 6 +-
.../ui/canvas/header/header.component.spec.ts | 6 +
.../create-process-group.component.ts | 1 +
.../service/parameter-contexts.service.ts | 4 -
.../feature/provenance-routing.module.ts} | 42 +-
.../provenance/feature/provenance.component.html | 28 ++
.../provenance/feature/provenance.component.scss} | 19 +-
.../feature/provenance.component.spec.ts | 48 +++
.../provenance/feature/provenance.component.ts | 42 ++
.../pages/provenance/feature/provenance.module.ts | 39 ++
.../pages/provenance/service/provenance.service.ts | 130 ++++++
.../src/app/{ => pages/provenance}/state/index.ts | 33 +-
.../state/provenance-event-listing/index.ts | 100 +++++
.../provenance-event-listing.actions.ts | 78 ++++
.../provenance-event-listing.effects.ts | 361 +++++++++++++++++
.../provenance-event-listing.reducer.ts | 71 ++++
.../provenance-event-listing.selectors.ts | 86 ++++
.../provenance-event-listing-routing.module.ts} | 30 +-
.../provenance-event-listing.component.html | 43 ++
.../provenance-event-listing.component.scss} | 19 -
.../provenance-event-listing.component.spec.ts | 41 ++
.../provenance-event-listing.component.ts | 177 ++++++++
.../provenance-event-listing.module.ts | 41 ++
.../provenance-event-table.component.html | 156 +++++++
.../provenance-event-table.component.scss} | 26 +-
.../provenance-event-table.component.spec.ts | 40 ++
.../provenance-event-table.component.ts | 232 +++++++++++
.../provenance-search-dialog.component.html | 76 ++++
.../provenance-search-dialog.component.scss} | 27 +-
.../provenance-search-dialog.component.spec.ts | 85 ++++
.../provenance-search-dialog.component.ts | 220 ++++++++++
.../{state/index.ts => service/about.service.ts} | 28 +-
.../nifi/src/app/service/nifi-common.service.ts | 145 +++++++
.../app/state/{index.ts => about/about.actions.ts} | 24 +-
.../main/nifi/src/app/state/about/about.effects.ts | 48 +++
.../main/nifi/src/app/state/about/about.reducer.ts | 50 +++
.../state/{index.ts => about/about.selectors.ts} | 20 +-
.../main/nifi/src/app/state/{ => about}/index.ts | 35 +-
.../src/main/nifi/src/app/state/index.ts | 6 +-
.../src/main/nifi/src/app/state/shared/index.ts | 64 +++
.../cancel-dialog/cancel-dialog.component.html | 24 ++
.../cancel-dialog/cancel-dialog.component.scss} | 19 -
.../cancel-dialog/cancel-dialog.component.spec.ts | 48 +++
.../cancel-dialog/cancel-dialog.component.ts} | 35 +-
.../property-table/property-table.component.ts | 8 +-
.../provenance-event-dialog.component.html | 447 +++++++++++++++++++++
.../provenance-event-dialog.component.scss} | 46 ++-
.../provenance-event-dialog.component.spec.ts | 85 ++++
.../provenance-event-dialog.component.ts | 98 +++++
.../src/main/nifi/src/styles.scss | 10 +-
67 files changed, 3579 insertions(+), 271 deletions(-)
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/proxy.config.mjs b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/proxy.config.mjs
index ee3441d494..1ac6ebf440 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/proxy.config.mjs
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/proxy.config.mjs
@@ -16,5 +16,24 @@ export default {
headers: {
'X-ProxyPort': 4200
}
+ },
+ '/nifi-content-viewer/*': {
+ target: 'https://localhost:8443',
+ secure: false,
+ logLevel: 'debug',
+ changeOrigin: true,
+ headers: {
+ 'X-ProxyPort': 4200
+ }
+ },
+ // the following entry is needed because the content viewer (and other UIs) load resources from existing nifi ui
+ '/nifi/*': {
+ target: 'https://localhost:8443',
+ secure: false,
+ logLevel: 'debug',
+ changeOrigin: true,
+ headers: {
+ 'X-ProxyPort': 4200
+ }
}
};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts
index 07361220c0..f70d4a9d40 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app-routing.module.ts
@@ -29,6 +29,11 @@ const routes: Routes = [
canMatch: [authGuard],
loadChildren: () => import('./pages/settings/feature/settings.module').then((m) => m.SettingsModule)
},
+ {
+ path: 'provenance',
+ canMatch: [authGuard],
+ loadChildren: () => import('./pages/provenance/feature/provenance.module').then((m) => m.ProvenanceModule)
+ },
{
path: 'parameter-contexts',
canMatch: [authGuard],
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
index 6afaea16c7..505e438dd4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/app.module.ts
@@ -20,7 +20,6 @@ import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-import { FlowDesignerModule } from './pages/flow-designer/feature/flow-designer.module';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
@@ -29,13 +28,14 @@ import { HTTP_INTERCEPTORS, HttpClientModule, HttpClientXsrfModule } from '@angu
import { NavigationActionTiming, RouterState, StoreRouterConnectingModule } from '@ngrx/router-store';
import { rootReducers } from './state';
import { UserEffects } from './state/user/user.effects';
-import { LoginModule } from './pages/login/feature/login.module';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { LoadingInterceptor } from './service/interceptors/loading.interceptor';
import { AuthInterceptor } from './service/interceptors/auth.interceptor';
import { ExtensionTypesEffects } from './state/extension-types/extension-types.effects';
import { PollingInterceptor } from './service/interceptors/polling.interceptor';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
+import { MatNativeDateModule } from '@angular/material/core';
+import { AboutEffects } from './state/about/about.effects';
// @ts-ignore
@NgModule({
@@ -54,13 +54,14 @@ import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
routerState: RouterState.Minimal,
navigationActionTiming: NavigationActionTiming.PostActivation
}),
- EffectsModule.forRoot(UserEffects, ExtensionTypesEffects),
+ EffectsModule.forRoot(UserEffects, ExtensionTypesEffects, AboutEffects),
StoreDevtoolsModule.instrument({
maxAge: 25,
logOnly: environment.production,
autoPause: true
}),
- MatProgressSpinnerModule
+ MatProgressSpinnerModule,
+ MatNativeDateModule
],
providers: [
{
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
index 7848b5a4ad..c161f9865c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/connectable-behavior.service.spec.ts
@@ -27,6 +27,8 @@ import { CanvasState } from '../../state';
import { transformFeatureKey } from '../../state/transform';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
describe('ConnectableBehavior', () => {
let service: ConnectableBehavior;
@@ -46,6 +48,10 @@ describe('ConnectableBehavior', () => {
{
selector: selectFlowState,
value: initialState[flowFeatureKey]
+ },
+ {
+ selector: selectUser,
+ value: fromUser.initialState.user
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
index 0d35ab7053..71042a1b90 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/draggable-behavior.service.spec.ts
@@ -28,6 +28,8 @@ import { transformFeatureKey } from '../../state/transform';
import { selectFlowState } from '../../state/flow/flow.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
describe('DraggableBehavior', () => {
let service: DraggableBehavior;
@@ -51,6 +53,10 @@ describe('DraggableBehavior', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
+ },
+ {
+ selector: selectUser,
+ value: fromUser.initialState.user
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
index b1257754ef..fab7e0f9b6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/behavior/quick-select-behavior.service.spec.ts
@@ -27,6 +27,8 @@ import { transformFeatureKey } from '../../state/transform';
import * as fromTransform from '../../state/transform/transform.reducer';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
describe('QuickSelectBehavior', () => {
let service: QuickSelectBehavior;
@@ -46,6 +48,10 @@ describe('QuickSelectBehavior', () => {
{
selector: selectFlowState,
value: initialState[flowFeatureKey]
+ },
+ {
+ selector: selectUser,
+ value: fromUser.initialState.user
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
index b1fb156735..93cf794068 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/birdseye-view.service.spec.ts
@@ -28,6 +28,8 @@ import { selectFlowState } from '../state/flow/flow.selectors';
import { selectTransform } from '../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../state/controller-services';
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../state/user/user.selectors';
+import * as fromUser from '../../../state/user/user.reducer';
describe('BirdseyeView', () => {
let service: BirdseyeView;
@@ -51,6 +53,10 @@ describe('BirdseyeView', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
+ },
+ {
+ selector: selectUser,
+ value: fromUser.initialState.user
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
index a184c5ddf2..5aa09cf287 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-utils.service.ts
@@ -26,12 +26,15 @@ import {
selectCurrentProcessGroupId,
selectParentProcessGroupId
} from '../state/flow/flow.selectors';
-import { initialState } from '../state/flow/flow.reducer';
+import { initialState as initialFlowState } from '../state/flow/flow.reducer';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { BulletinsTip } from '../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
import { Position } from '../state/shared';
import { ComponentType, Permissions } from '../../../state/shared';
import { NiFiCommon } from '../../../service/nifi-common.service';
+import { User } from '../../../state/user';
+import { initialState as initialUserState } from '../../../state/user/user.reducer';
+import { selectUser } from '../../../state/user/user.selectors';
@Injectable({
providedIn: 'root'
@@ -42,9 +45,10 @@ export class CanvasUtils {
private destroyRef = inject(DestroyRef);
private trimLengthCaches: Map<string, Map<string, Map<number, number>>> = new Map();
- private currentProcessGroupId: string = initialState.id;
- private parentProcessGroupId: string | null = initialState.flow.processGroupFlow.parentGroupId;
- private canvasPermissions: Permissions = initialState.flow.permissions;
+ private currentProcessGroupId: string = initialFlowState.id;
+ private parentProcessGroupId: string | null = initialFlowState.flow.processGroupFlow.parentGroupId;
+ private canvasPermissions: Permissions = initialFlowState.flow.permissions;
+ private currentUser: User = initialUserState.user;
private connections: any[] = [];
private readonly humanizeDuration: Humanizer;
@@ -83,6 +87,13 @@ export class CanvasUtils {
.subscribe((connections) => {
this.connections = connections;
});
+
+ this.store
+ .select(selectUser)
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((user) => {
+ this.currentUser = user;
+ });
}
public hasDownstream(selection: any): boolean {
@@ -124,6 +135,13 @@ export class CanvasUtils {
d3.select('path.connector').remove();
}
+ /**
+ * Determines whether the current user can access provenance.
+ */
+ public canAccessProvenance(): boolean {
+ return this.currentUser.provenancePermissions.canRead;
+ }
+
/**
* Determines whether the specified selection is empty.
*
@@ -435,6 +453,40 @@ export class CanvasUtils {
return selection.size() === 1 && selection.classed('funnel');
}
+ /**
+ * Determines whether the current user can access provenance for the specified component.
+ *
+ * @argument {selection} selection The selection
+ */
+ public canAccessComponentProvenance(selection: any): boolean {
+ // ensure the correct number of components are selected
+ if (selection.size() !== 1) {
+ return false;
+ }
+
+ return (
+ !this.isLabel(selection) &&
+ !this.isConnection(selection) &&
+ !this.isProcessGroup(selection) &&
+ !this.isRemoteProcessGroup(selection) &&
+ this.canAccessProvenance()
+ );
+ }
+
+ /**
+ * Determines whether the current selection should provide ability to replay latest provenance event.
+ *
+ * @param {selection} selection
+ */
+ public canReplayComponentProvenance(selection: any): boolean {
+ // ensure the correct number of components are selected
+ if (selection.size() !== 1) {
+ return false;
+ }
+
+ return this.isProcessor(selection) && this.canAccessProvenance();
+ }
+
/**
* Gets the currently selected components and connections.
*
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
index 21e166cc3a..77aa95ca71 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/canvas-view.service.spec.ts
@@ -28,6 +28,8 @@ import { selectFlowState } from '../state/flow/flow.selectors';
import { selectTransform } from '../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../state/controller-services';
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../state/user/user.selectors';
+import * as fromUser from '../../../state/user/user.reducer';
describe('CanvasView', () => {
let service: CanvasView;
@@ -51,6 +53,10 @@ describe('CanvasView', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
+ },
+ {
+ selector: selectUser,
+ value: fromUser.initialState.user
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts
index 53df2eb224..6d56999806 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/flow.service.ts
@@ -26,6 +26,7 @@ import {
CreateProcessGroupRequest,
CreateProcessorRequest,
DeleteComponentRequest,
+ ReplayLastProvenanceEventRequest,
Snippet,
UpdateComponentRequest,
UploadProcessGroupRequest
@@ -95,10 +96,6 @@ export class FlowService {
});
}
- getCurrentUser(): Observable<any> {
- return this.httpClient.get(`${FlowService.API}/flow/controller/bulletins`);
- }
-
getProcessor(id: string): Observable<any> {
return this.httpClient.get(`${FlowService.API}/processors/${id}`);
}
@@ -245,4 +242,8 @@ export class FlowService {
deleteSnippet(snippetId: string): Observable<any> {
return this.httpClient.delete(`${FlowService.API}/snippets/${snippetId}`);
}
+
+ replayLastProvenanceEvent(request: ReplayLastProvenanceEventRequest): Observable<any> {
+ return this.httpClient.post(`${FlowService.API}/provenance-events/latest/replays`, request);
+ }
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
index 5ecd9dd8c0..222784270e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/connection-manager.service.spec.ts
@@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
describe('ConnectionManager', () => {
let service: ConnectionManager;
@@ -51,6 +53,10 @@ describe('ConnectionManager', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
+ },
+ {
+ selector: selectUser,
+ value: fromUser.initialState.user
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
index 155847e2d2..eff69edad4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/funnel-manager.service.spec.ts
@@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
describe('FunnelManager', () => {
let service: FunnelManager;
@@ -51,6 +53,10 @@ describe('FunnelManager', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
+ },
+ {
+ selector: selectUser,
+ value: fromUser.initialState.user
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
index 422f1a41f9..8bb03c75ef 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/service/manager/remote-process-group-manager.service.spec.ts
@@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
import { selectTransform } from '../../state/transform/transform.selectors';
import { controllerServicesFeatureKey } from '../../state/controller-services';
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
+import { selectUser } from '../../../../state/user/user.selectors';
+import * as fromUser from '../../../../state/user/user.reducer';
describe('RemoteProcessGroupManager', () => {
let service: RemoteProcessGroupManager;
@@ -51,6 +53,10 @@ describe('RemoteProcessGroupManager', () => {
{
selector: selectTransform,
value: initialState[transformFeatureKey]
+ },
+ {
+ selector: selectUser,
+ value: fromUser.initialState.user
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
index 49fbb1e428..52ba23be40 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
@@ -53,7 +53,8 @@ import {
UpdatePositionsRequest,
UploadProcessGroupRequest,
EditCurrentProcessGroupRequest,
- NavigateToControllerServicesRequest
+ NavigateToControllerServicesRequest,
+ ReplayLastProvenanceEventRequest
} from './index';
/*
@@ -375,3 +376,13 @@ export const renderConnectionsForComponent = createAction(
'[Canvas] Render Connections For Component',
props<{ id: string; updatePath: boolean; updateLabel: boolean }>()
);
+
+export const navigateToProvenanceForComponent = createAction(
+ '[Canvas] Navigate To Provenance For Component',
+ props<{ id: string }>()
+);
+
+export const replayLastProvenanceEvent = createAction(
+ '[Canvas] Replay Last Provenance Event',
+ props<{ request: ReplayLastProvenanceEventRequest }>()
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index 623bf47a54..c7f255f941 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -1770,6 +1770,94 @@ export class FlowEffects {
{ dispatch: false }
);
+ navigateToProvenanceForComponent$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(FlowActions.navigateToProvenanceForComponent),
+ map((action) => action.id),
+ tap((componentId) => {
+ this.router.navigate(['/provenance'], { queryParams: { componentId } });
+ })
+ ),
+ { dispatch: false }
+ );
+
+ replayLastProvenanceEvent = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(FlowActions.replayLastProvenanceEvent),
+ map((action) => action.request),
+ tap((request) => {
+ this.flowService.replayLastProvenanceEvent(request).subscribe({
+ next: (response) => {
+ if (response.aggregateSnapshot.failureExplanation) {
+ this.store.dispatch(
+ FlowActions.showOkDialog({
+ title: 'Replay Event Failure',
+ message: response.aggregateSnapshot.failureExplanation
+ })
+ );
+ } else if (response.aggregateSnapshot.eventAvailable !== true) {
+ this.store.dispatch(
+ FlowActions.showOkDialog({
+ title: 'No Event Available',
+ message: 'There was no recent event available to be replayed.'
+ })
+ );
+ } else if (response.nodeSnapshots) {
+ let replayedCount: number = 0;
+ let unavailableCount: number = 0;
+
+ response.nodeSnapshots.forEach((nodeResponse: any) => {
+ if (nodeResponse.snapshot.eventAvailable) {
+ replayedCount++;
+ } else {
+ unavailableCount++;
+ }
+ });
+
+ let message: string;
+ if (unavailableCount === 0) {
+ message = 'All nodes successfully replayed the latest event.';
+ } else {
+ message = `${replayedCount} nodes successfully replayed the latest event but ${unavailableCount} had no recent event available to be replayed.`;
+ }
+
+ this.store.dispatch(
+ FlowActions.showOkDialog({
+ title: 'Events Replayed',
+ message
+ })
+ );
+ } else {
+ this.store.dispatch(
+ FlowActions.showOkDialog({
+ title: 'Events Replayed',
+ message: 'Successfully replayed the latest event.'
+ })
+ );
+ }
+
+ this.store.dispatch(
+ FlowActions.loadConnectionsForComponent({
+ id: request.componentId
+ })
+ );
+ },
+ error: (error) => {
+ this.store.dispatch(
+ FlowActions.showOkDialog({
+ title: 'Failed to Replay Event',
+ message: error.error
+ })
+ );
+ }
+ });
+ })
+ ),
+ { dispatch: false }
+ );
+
showOkDialog$ = createEffect(
() =>
this.actions$.pipe(
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
index 08c25e4dad..47c8f56eef 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/state/flow/index.ts
@@ -304,6 +304,11 @@ export interface NavigateToComponentRequest {
processGroupId?: string;
}
+export interface ReplayLastProvenanceEventRequest {
+ componentId: string;
+ nodes: string;
+}
+
/*
Snippets
*/
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/context-menu/context-menu.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/context-menu/context-menu.component.ts
index 5ac2010bd9..d02892d948 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/context-menu/context-menu.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/context-menu/context-menu.component.ts
@@ -30,7 +30,9 @@ import {
navigateToControllerServicesForProcessGroup,
navigateToEditComponent,
navigateToEditCurrentProcessGroup,
- reloadFlow
+ navigateToProvenanceForComponent,
+ reloadFlow,
+ replayLastProvenanceEvent
} from '../../../state/flow/flow.actions';
import { CanvasUtils } from '../../../service/canvas-utils.service';
import { DeleteComponentRequest, MoveComponentRequest } from '../../../state/flow';
@@ -154,24 +156,38 @@ export class ContextMenu implements OnInit {
menuItems: [
{
condition: function (canvasUtils: CanvasUtils, selection: any) {
- // TODO - canReplayProvenance
- return false;
+ return canvasUtils.canReplayComponentProvenance(selection);
},
clazz: 'fa',
text: 'All nodes',
- action: function (store: Store<CanvasState>) {
- // TODO - replayLastAllNodes
+ action: function (store: Store<CanvasState>, selection: any) {
+ const selectionData = selection.datum();
+ store.dispatch(
+ replayLastProvenanceEvent({
+ request: {
+ componentId: selectionData.id,
+ nodes: 'ALL'
+ }
+ })
+ );
}
},
{
condition: function (canvasUtils: CanvasUtils, selection: any) {
- // TODO - canReplayProvenance
- return false;
+ return canvasUtils.canReplayComponentProvenance(selection);
},
clazz: 'fa',
text: 'Primary node',
- action: function (store: Store<CanvasState>) {
- // TODO - replayLastPrimaryNode
+ action: function (store: Store<CanvasState>, selection: any) {
+ const selectionData = selection.datum();
+ store.dispatch(
+ replayLastProvenanceEvent({
+ request: {
+ componentId: selectionData.id,
+ nodes: 'PRIMARY'
+ }
+ })
+ );
}
}
]
@@ -525,14 +541,18 @@ export class ContextMenu implements OnInit {
},
{
condition: function (canvasUtils: CanvasUtils, selection: any) {
- // TODO - canAccessProvenance
- return false;
+ return canvasUtils.canAccessComponentProvenance(selection);
},
clazz: 'icon icon-provenance',
// imgStyle: 'context-menu-provenance',
text: 'View data provenance',
- action: function (store: Store<CanvasState>) {
- // TODO - openProvenance
+ action: function (store: Store<CanvasState>, selection: any) {
+ const selectionData = selection.datum();
+ store.dispatch(
+ navigateToProvenanceForComponent({
+ id: selectionData.id
+ })
+ );
}
},
{
@@ -958,6 +978,7 @@ export class ContextMenu implements OnInit {
) {
this.allMenus = new Map<string, ContextMenuDefinition>();
this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);
+ this.allMenus.set(this.PROVENANCE_REPLAY.id, this.PROVENANCE_REPLAY);
this.allMenus.set(this.VERSION_MENU.id, this.VERSION_MENU);
this.allMenus.set(this.UPSTREAM_DOWNSTREAM.id, this.UPSTREAM_DOWNSTREAM);
this.allMenus.set(this.ALIGN.id, this.ALIGN);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html
index f0c9bd2d0b..f3d27c6ee0 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.html
@@ -81,7 +81,11 @@
Bulletin Board
</button>
<mat-divider></mat-divider>
- <button mat-menu-item class="global-menu-item">
+ <button
+ mat-menu-item
+ class="global-menu-item"
+ [routerLink]="['/provenance']"
+ [disabled]="!user.provenancePermissions.canRead">
<i class="icon fa-fw icon-provenance mr-2"></i>
Data Provenance
</button>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.spec.ts
index b836f11894..fb47ed315d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.spec.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/header/header.component.spec.ts
@@ -36,6 +36,8 @@ import { ClusterSummary, ControllerStatus } from '../../../state/flow';
import { Search } from './search/search.component';
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { selectUser } from '../../../../../state/user/user.selectors';
+import * as fromUser from '../../../../../state/user/user.reducer';
describe('HeaderComponent', () => {
let component: HeaderComponent;
@@ -99,6 +101,10 @@ describe('HeaderComponent', () => {
{
selector: selectControllerBulletins,
value: []
+ },
+ {
+ selector: selectUser,
+ value: fromUser.initialState.user
}
]
})
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.ts
index 608f3aaeec..74b8865aed 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/flow-designer/ui/canvas/items/process-group/create-process-group/create-process-group.component.ts
@@ -114,6 +114,7 @@ export class CreateProcessGroup {
this.createProcessGroupForm
.get('newProcessGroupName')
?.setValue(this.nifiCommon.substringBeforeLast(file.name, '.'));
+ this.createProcessGroupForm.get('newProcessGroupName')?.markAsDirty();
this.createProcessGroupForm.get('newProcessGroupParameterContext')?.setValue(null);
this.flowNameAttached = file.name;
this.flowDefinition = file;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/service/parameter-contexts.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/service/parameter-contexts.service.ts
index deb0e86b0d..01dd297cdc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/service/parameter-contexts.service.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/parameter-contexts/service/parameter-contexts.service.ts
@@ -92,8 +92,4 @@ export class ParameterContextService {
params: revision
});
}
-
- // updateControllerConfig(controllerEntity: ControllerEntity): Observable<any> {
- // return this.httpClient.put(`${ControllerServiceService.API}/controller/config`, controllerEntity);
- // }
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance-routing.module.ts
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance-routing.module.ts
index 3789433cd3..3dc770a7a7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance-routing.module.ts
@@ -15,21 +15,31 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { Provenance } from './provenance.component';
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+const routes: Routes = [
+ {
+ path: '',
+ component: Provenance,
+ children: [
+ // {
+ // path: 'lineage'
+ // },
+ {
+ path: '',
+ loadChildren: () =>
+ import('../ui/provenance-event-listing/provenance-event-listing.module').then(
+ (m) => m.ProvenanceEventListingModule
+ )
+ }
+ ]
+ }
+];
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class ProvenanceRoutingModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.html
new file mode 100644
index 0000000000..f14af50f77
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.html
@@ -0,0 +1,28 @@
+<!--
+ ~ 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.
+ -->
+
+<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
+ <div class="flex justify-between">
+ <h3 class="text-xl bold provenance-header">Provenance</h3>
+ <button class="nifi-button" [routerLink]="['/']">
+ <i class="fa fa-times"></i>
+ </button>
+ </div>
+ <div class="flex-1">
+ <router-outlet></router-outlet>
+ </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.scss
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.scss
index 3789433cd3..288912eaa7 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.scss
@@ -15,21 +15,6 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
-
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
+.provenance-header {
+ color: #728e9b;
}
-
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.spec.ts
new file mode 100644
index 0000000000..200affe6d8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.spec.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Provenance } from './provenance.component';
+import { provideMockStore } from '@ngrx/store/testing';
+import { RouterModule } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+import { initialState } from '../state/provenance-event-listing/provenance-event-listing.reducer';
+
+describe('Provenance', () => {
+ let component: Provenance;
+ let fixture: ComponentFixture<Provenance>;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [Provenance],
+ imports: [RouterModule, RouterTestingModule],
+ providers: [
+ provideMockStore({
+ initialState
+ })
+ ]
+ });
+ fixture = TestBed.createComponent(Provenance);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.ts
new file mode 100644
index 0000000000..3ae2282b2c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.component.ts
@@ -0,0 +1,42 @@
+/*
+ * 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, OnDestroy, OnInit } from '@angular/core';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../state';
+import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
+import { loadProvenanceOptions } from '../state/provenance-event-listing/provenance-event-listing.actions';
+import { loadAbout } from '../../../state/about/about.actions';
+
+@Component({
+ selector: 'provenance',
+ templateUrl: './provenance.component.html',
+ styleUrls: ['./provenance.component.scss']
+})
+export class Provenance implements OnInit, OnDestroy {
+ constructor(private store: Store<NiFiState>) {}
+
+ ngOnInit(): void {
+ this.store.dispatch(startUserPolling());
+ this.store.dispatch(loadProvenanceOptions());
+ this.store.dispatch(loadAbout());
+ }
+
+ ngOnDestroy(): void {
+ this.store.dispatch(stopUserPolling());
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.module.ts
new file mode 100644
index 0000000000..be2c442b48
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/feature/provenance.module.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { StoreModule } from '@ngrx/store';
+import { EffectsModule } from '@ngrx/effects';
+import { Provenance } from './provenance.component';
+import { ProvenanceRoutingModule } from './provenance-routing.module';
+import { provenanceFeatureKey, reducers } from '../state';
+import { ProvenanceEventListingEffects } from '../state/provenance-event-listing/provenance-event-listing.effects';
+import { MatDialogModule } from '@angular/material/dialog';
+
+@NgModule({
+ declarations: [Provenance],
+ exports: [Provenance],
+ imports: [
+ CommonModule,
+ MatDialogModule,
+ ProvenanceRoutingModule,
+ StoreModule.forFeature(provenanceFeatureKey, reducers),
+ EffectsModule.forFeature(ProvenanceEventListingEffects)
+ ]
+})
+export class ProvenanceModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/service/provenance.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/service/provenance.service.ts
new file mode 100644
index 0000000000..7d831f9444
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/service/provenance.service.ts
@@ -0,0 +1,130 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { Observable, throwError } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
+import { NiFiCommon } from '../../../service/nifi-common.service';
+import { ProvenanceRequest } from '../state/provenance-event-listing';
+
+@Injectable({ providedIn: 'root' })
+export class ProvenanceService {
+ private static readonly API: string = '../nifi-api';
+
+ constructor(
+ private httpClient: HttpClient,
+ private nifiCommon: NiFiCommon
+ ) {}
+
+ /**
+ * The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
+ * does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
+ *
+ * https://stackoverflow.com/a/59586462
+ *
+ * @param url
+ * @private
+ */
+ private stripProtocol(url: string): string {
+ return this.nifiCommon.substringAfterFirst(url, ':');
+ }
+
+ getSearchOptions(): Observable<any> {
+ return this.httpClient.get(`${ProvenanceService.API}/provenance/search-options`);
+ }
+
+ submitProvenanceQuery(request: ProvenanceRequest): Observable<any> {
+ return this.httpClient.post(`${ProvenanceService.API}/provenance`, { provenance: { request } });
+ }
+
+ getProvenanceQuery(id: string, clusterNodeId?: string): Observable<any> {
+ // TODO - cluster node id
+ return this.httpClient.get(`${ProvenanceService.API}/provenance/${encodeURIComponent(id)}`);
+ }
+
+ deleteProvenanceQuery(id: string, clusterNodeId?: string): Observable<any> {
+ // TODO - cluster node id
+ return this.httpClient.delete(`${ProvenanceService.API}/provenance/${encodeURIComponent(id)}`);
+ }
+
+ getProvenanceEvent(id: string): Observable<any> {
+ // TODO - cluster node id
+ return this.httpClient.get(`${ProvenanceService.API}/provenance-events/${encodeURIComponent(id)}`);
+ }
+
+ downloadContent(id: string, direction: string): void {
+ let dataUri: string = `${ProvenanceService.API}/provenance-events/${encodeURIComponent(
+ id
+ )}/content/${encodeURIComponent(direction)}`;
+
+ const queryParameters: any = {};
+
+ // TODO - cluster node id in query parameters
+
+ if (Object.keys(queryParameters).length > 0) {
+ const query: string = new URLSearchParams(queryParameters).toString();
+ dataUri = `${dataUri}?${query}`;
+ }
+
+ window.open(dataUri);
+ }
+
+ viewContent(nifiUrl: string, contentViewerUrl: string, id: string, direction: string): void {
+ // build the uri to the data
+ let dataUri: string = `${nifiUrl}provenance-events/${encodeURIComponent(id)}/content/${encodeURIComponent(
+ direction
+ )}`;
+
+ const dataUriParameters: any = {};
+
+ // TODO - cluster node id in data uri parameters
+
+ // include parameters if necessary
+ if (Object.keys(dataUriParameters).length > 0) {
+ const dataUriQuery: string = new URLSearchParams(dataUriParameters).toString();
+ dataUri = `${dataUri}?${dataUriQuery}`;
+ }
+
+ // if there's already a query string don't add another ?... this assumes valid
+ // input meaning that if the url has already included a ? it also contains at
+ // least one query parameter
+ let contentViewer: string = contentViewerUrl;
+ if (contentViewer.indexOf('?') === -1) {
+ contentViewer += '?';
+ } else {
+ contentViewer += '&';
+ }
+
+ const contentViewerParameters: any = {
+ ref: dataUri
+ };
+
+ // open the content viewer
+ const contentViewerQuery: string = new URLSearchParams(contentViewerParameters).toString();
+ window.open(`${contentViewer}${contentViewerQuery}`);
+ }
+
+ replay(eventId: string): Observable<any> {
+ const payload: any = {
+ eventId
+ };
+
+ // TODO - add cluster node id in payload
+
+ return this.httpClient.post(`${ProvenanceService.API}/provenance-events/replays`, payload);
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/index.ts
similarity index 50%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/index.ts
index 3789433cd3..0457b6a4b6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/index.ts
@@ -15,21 +15,24 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+/*
+ Parameter Contexts
+ */
+
+import { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
+import { provenanceEventListingFeatureKey, ProvenanceEventListingState } from './provenance-event-listing';
+import { provenanceEventListingReducer } from './provenance-event-listing/provenance-event-listing.reducer';
+
+export const provenanceFeatureKey = 'provenance';
+
+export interface ProvenanceState {
+ [provenanceEventListingFeatureKey]: ProvenanceEventListingState;
+}
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
+export function reducers(state: ProvenanceState | undefined, action: Action) {
+ return combineReducers({
+ [provenanceEventListingFeatureKey]: provenanceEventListingReducer
+ })(state, action);
}
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
+export const selectProvenanceState = createFeatureSelector<ProvenanceState>(provenanceFeatureKey);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/index.ts
new file mode 100644
index 0000000000..7ffd66a323
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/index.ts
@@ -0,0 +1,100 @@
+/*
+ * 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 { ProvenanceEventSummary } from '../../../../state/shared';
+
+export const provenanceEventListingFeatureKey = 'provenanceEventListing';
+
+export interface ProvenanceOptionsResponse {
+ provenanceOptions: ProvenanceOptions;
+}
+
+export interface ProvenanceQueryParams {
+ flowFileUuid?: string;
+ componentId?: string;
+}
+
+export interface ProvenanceQueryResponse {
+ provenance: Provenance;
+}
+
+export interface SearchableField {
+ field: string;
+ id: string;
+ label: string;
+ type: string;
+}
+
+export interface ProvenanceOptions {
+ searchableFields: SearchableField[];
+}
+
+export interface ProvenanceSearchDialogRequest {
+ timeOffset: number;
+ options: ProvenanceOptions;
+ currentRequest: ProvenanceRequest;
+}
+
+export interface ProvenanceSearchValue {
+ value: string;
+ inverse: boolean;
+}
+
+export interface ProvenanceRequest {
+ searchTerms?: {
+ [key: string]: ProvenanceSearchValue;
+ };
+ clusterNodeId?: string;
+ startDate?: string;
+ endDate?: string;
+ minimumFileSize?: string;
+ maximumFileSize?: string;
+ maxResults: number;
+ summarize: boolean;
+ incrementalResults: boolean;
+}
+
+export interface ProvenanceResults {
+ provenanceEvents: ProvenanceEventSummary[];
+ total: string;
+ totalCount: number;
+ generated: string;
+ oldestEvent: string;
+ timeOffset: number;
+ errors: string[];
+}
+
+export interface Provenance {
+ id: string;
+ uri: string;
+ submissionTime: string;
+ expiration: string;
+ percentCompleted: number;
+ finished: boolean;
+ request: ProvenanceRequest;
+ results: ProvenanceResults;
+}
+
+export interface ProvenanceEventListingState {
+ options: ProvenanceOptions | null;
+ request: ProvenanceRequest | null;
+ provenance: Provenance | null;
+ saving: boolean;
+ loadedTimestamp: string;
+ error: string | null;
+ status: 'pending' | 'loading' | 'error' | 'success';
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.actions.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.actions.ts
new file mode 100644
index 0000000000..00ab086552
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.actions.ts
@@ -0,0 +1,78 @@
+/*
+ * 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 { createAction, props } from '@ngrx/store';
+import { ProvenanceOptionsResponse, ProvenanceQueryResponse, ProvenanceRequest } from './index';
+
+export const loadProvenanceOptions = createAction('[Provenance Event Listing] Load Provenance Options');
+
+export const loadProvenanceOptionsSuccess = createAction(
+ '[Provenance Event Listing] Load Provenance Options Success',
+ props<{ response: ProvenanceOptionsResponse }>()
+);
+
+export const submitProvenanceQuery = createAction(
+ '[Provenance Event Listing] Submit Provenance Query',
+ props<{ request: ProvenanceRequest }>()
+);
+
+export const resubmitProvenanceQuery = createAction(
+ '[Provenance Event Listing] Resubmit Provenance Query',
+ props<{ request: ProvenanceRequest }>()
+);
+
+export const submitProvenanceQuerySuccess = createAction(
+ '[Provenance Event Listing] Submit Provenance Query Success',
+ props<{ response: ProvenanceQueryResponse }>()
+);
+
+export const startPollingProvenanceQuery = createAction('[Provenance Event Listing] Start Polling Provenance Query');
+
+export const pollProvenanceQuery = createAction('[Provenance Event Listing] Poll Provenance Query');
+
+export const pollProvenanceQuerySuccess = createAction(
+ '[Provenance Event Listing] Poll Provenance Query Success',
+ props<{ response: ProvenanceQueryResponse }>()
+);
+
+export const stopPollingProvenanceQuery = createAction('[Provenance Event Listing] Stop Polling Provenance Query');
+
+export const deleteProvenanceQuery = createAction('[Provenance Event Listing] Delete Provenance Query');
+
+export const provenanceApiError = createAction(
+ '[Provenance Event Listing] Load Parameter Context Listing Error',
+ props<{ error: string }>()
+);
+
+export const openProvenanceEventDialog = createAction(
+ '[Provenance Event Listing] Open Provenance Event Dialog',
+ props<{ id: string }>()
+);
+
+export const openSearchDialog = createAction('[Provenance Event Listing] Open Search Dialog');
+
+export const saveProvenanceRequest = createAction(
+ '[Provenance Event Listing] Save Provenance Request',
+ props<{ request: ProvenanceRequest }>()
+);
+
+export const clearProvenanceRequest = createAction('[Provenance Event Listing] Clear Provenance Request');
+
+export const showOkDialog = createAction(
+ '[Provenance Event Listing] Show Ok Dialog',
+ props<{ title: string; message: string }>()
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.effects.ts
new file mode 100644
index 0000000000..4c3725b226
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.effects.ts
@@ -0,0 +1,361 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import * as ProvenanceEventListingActions from './provenance-event-listing.actions';
+import {
+ asyncScheduler,
+ catchError,
+ from,
+ interval,
+ map,
+ NEVER,
+ of,
+ switchMap,
+ take,
+ takeUntil,
+ tap,
+ withLatestFrom
+} from 'rxjs';
+import { MatDialog } from '@angular/material/dialog';
+import { Store } from '@ngrx/store';
+import { NiFiState } from '../../../../state';
+import { Router } from '@angular/router';
+import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
+import { ProvenanceService } from '../../service/provenance.service';
+import {
+ selectClusterNodeId,
+ selectProvenanceId,
+ selectProvenanceOptions,
+ selectProvenanceRequest,
+ selectTimeOffset
+} from './provenance-event-listing.selectors';
+import { Provenance, ProvenanceRequest } from './index';
+import { ProvenanceSearchDialog } from '../../ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component';
+import { selectAbout } from '../../../../state/about/about.selectors';
+import { ProvenanceEventDialog } from '../../../../ui/common/provenance-event-dialog/provenance-event-dialog.component';
+import { CancelDialog } from '../../../../ui/common/cancel-dialog/cancel-dialog.component';
+
+@Injectable()
+export class ProvenanceEventListingEffects {
+ constructor(
+ private actions$: Actions,
+ private store: Store<NiFiState>,
+ private provenanceService: ProvenanceService,
+ private dialog: MatDialog,
+ private router: Router
+ ) {}
+
+ loadProvenanceOptions$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.loadProvenanceOptions),
+ switchMap((request) =>
+ from(this.provenanceService.getSearchOptions()).pipe(
+ map((response) =>
+ ProvenanceEventListingActions.loadProvenanceOptionsSuccess({
+ response
+ })
+ ),
+ catchError((error) =>
+ of(
+ ProvenanceEventListingActions.provenanceApiError({
+ error: error.error
+ })
+ )
+ )
+ )
+ )
+ )
+ );
+
+ submitProvenanceQuery$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.submitProvenanceQuery),
+ map((action) => action.request),
+ switchMap((request) =>
+ from(this.provenanceService.submitProvenanceQuery(request)).pipe(
+ map((response) =>
+ ProvenanceEventListingActions.submitProvenanceQuerySuccess({
+ response: {
+ provenance: response.provenance
+ }
+ })
+ ),
+ catchError((error) => {
+ this.store.dispatch(
+ ProvenanceEventListingActions.showOkDialog({
+ title: 'Error',
+ message: error.error
+ })
+ );
+
+ return of(
+ ProvenanceEventListingActions.provenanceApiError({
+ error: error.error
+ })
+ );
+ })
+ )
+ )
+ )
+ );
+
+ resubmitProvenanceQuery = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.resubmitProvenanceQuery),
+ map((action) => action.request),
+ switchMap((request) => {
+ const dialogReference = this.dialog.open(CancelDialog, {
+ data: {
+ title: 'Provenance',
+ message: 'Searching provenance events...'
+ },
+ disableClose: true,
+ panelClass: 'small-dialog'
+ });
+
+ dialogReference.componentInstance.cancel.pipe(take(1)).subscribe(() => {
+ this.store.dispatch(ProvenanceEventListingActions.stopPollingProvenanceQuery());
+ });
+
+ return of(ProvenanceEventListingActions.submitProvenanceQuery({ request }));
+ })
+ )
+ );
+
+ submitProvenanceQuerySuccess$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.submitProvenanceQuerySuccess),
+ map((action) => action.response),
+ switchMap((response) => {
+ const query: Provenance = response.provenance;
+ if (query.finished) {
+ this.dialog.closeAll();
+ return of(ProvenanceEventListingActions.deleteProvenanceQuery());
+ } else {
+ return of(ProvenanceEventListingActions.startPollingProvenanceQuery());
+ }
+ })
+ )
+ );
+
+ startPollingProvenanceQuery$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.startPollingProvenanceQuery),
+ switchMap(() =>
+ interval(2000, asyncScheduler).pipe(
+ takeUntil(this.actions$.pipe(ofType(ProvenanceEventListingActions.stopPollingProvenanceQuery)))
+ )
+ ),
+ switchMap(() => of(ProvenanceEventListingActions.pollProvenanceQuery()))
+ )
+ );
+
+ pollProvenanceQuery$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.pollProvenanceQuery),
+ withLatestFrom(this.store.select(selectProvenanceId), this.store.select(selectClusterNodeId)),
+ switchMap(([action, id, clusterNodeId]) => {
+ if (id) {
+ return from(this.provenanceService.getProvenanceQuery(id, clusterNodeId)).pipe(
+ map((response) =>
+ ProvenanceEventListingActions.pollProvenanceQuerySuccess({
+ response: {
+ provenance: response.provenance
+ }
+ })
+ ),
+ catchError((error) =>
+ of(
+ ProvenanceEventListingActions.provenanceApiError({
+ error: error.error
+ })
+ )
+ )
+ );
+ } else {
+ return NEVER;
+ }
+ })
+ )
+ );
+
+ pollProvenanceQuerySuccess$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.pollProvenanceQuerySuccess),
+ map((action) => action.response),
+ switchMap((response) => {
+ const query: Provenance = response.provenance;
+ if (query.finished) {
+ this.dialog.closeAll();
+ return of(ProvenanceEventListingActions.stopPollingProvenanceQuery());
+ } else {
+ return NEVER;
+ }
+ })
+ )
+ );
+
+ stopPollingProvenanceQuery$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.stopPollingProvenanceQuery),
+ switchMap((response) => of(ProvenanceEventListingActions.deleteProvenanceQuery()))
+ )
+ );
+
+ deleteProvenanceQuery$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.deleteProvenanceQuery),
+ withLatestFrom(this.store.select(selectProvenanceId), this.store.select(selectClusterNodeId)),
+ tap(([action, id, clusterNodeId]) => {
+ if (id) {
+ this.provenanceService.deleteProvenanceQuery(id, clusterNodeId).subscribe();
+ }
+ })
+ ),
+ { dispatch: false }
+ );
+
+ openSearchDialog$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.openSearchDialog),
+ withLatestFrom(
+ this.store.select(selectTimeOffset),
+ this.store.select(selectProvenanceOptions),
+ this.store.select(selectProvenanceRequest),
+ this.store.select(selectAbout)
+ ),
+ tap(([request, timeOffset, options, currentRequest, about]) => {
+ if (about) {
+ const dialogReference = this.dialog.open(ProvenanceSearchDialog, {
+ data: {
+ timeOffset,
+ options,
+ currentRequest
+ },
+ panelClass: 'large-dialog'
+ });
+
+ dialogReference.componentInstance.timezone = about.timezone;
+
+ dialogReference.componentInstance.submitSearchCriteria
+ .pipe(take(1))
+ .subscribe((request: ProvenanceRequest) => {
+ if (request.searchTerms) {
+ const queryParams: any = {};
+ if (request.searchTerms['ProcessorID']) {
+ queryParams['componentId'] = request.searchTerms['ProcessorID'].value;
+ }
+ if (request.searchTerms['FlowFileUUID']) {
+ queryParams['flowFileUuid'] = request.searchTerms['FlowFileUUID'].value;
+ }
+
+ // if either of the supported query params are present in the query, update the url
+ if (Object.keys(queryParams).length > 0) {
+ this.router.navigate(['/provenance'], { queryParams });
+ }
+ }
+
+ this.store.dispatch(ProvenanceEventListingActions.saveProvenanceRequest({ request }));
+ });
+ }
+
+ // TODO - if about hasn't loaded we should show an error
+ })
+ ),
+ { dispatch: false }
+ );
+
+ openProvenanceEventDialog$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.openProvenanceEventDialog),
+ withLatestFrom(this.store.select(selectAbout)),
+ tap(([request, about]) => {
+ this.provenanceService.getProvenanceEvent(request.id).subscribe({
+ next: (response) => {
+ const dialogReference = this.dialog.open(ProvenanceEventDialog, {
+ data: {
+ event: response.provenanceEvent
+ },
+ panelClass: 'large-dialog'
+ });
+
+ dialogReference.componentInstance.contentViewerAvailable =
+ about?.contentViewerUrl != null ?? false;
+
+ dialogReference.componentInstance.downloadContent
+ .pipe(takeUntil(dialogReference.afterClosed()))
+ .subscribe((direction: string) => {
+ this.provenanceService.downloadContent(request.id, direction);
+ });
+
+ if (about) {
+ dialogReference.componentInstance.viewContent
+ .pipe(takeUntil(dialogReference.afterClosed()))
+ .subscribe((direction: string) => {
+ this.provenanceService.viewContent(
+ about.uri,
+ about.contentViewerUrl,
+ request.id,
+ direction
+ );
+ });
+ }
+
+ dialogReference.componentInstance.replay
+ .pipe(takeUntil(dialogReference.afterClosed()))
+ .subscribe(() => {
+ this.provenanceService.replay(request.id).subscribe(() => {
+ this.store.dispatch(
+ ProvenanceEventListingActions.showOkDialog({
+ title: 'Provenance',
+ message: 'Successfully submitted replay request.'
+ })
+ );
+ });
+ });
+ },
+ error: (error) => {
+ // TODO - handle error
+ }
+ });
+ })
+ ),
+ { dispatch: false }
+ );
+
+ showOkDialog$ = createEffect(
+ () =>
+ this.actions$.pipe(
+ ofType(ProvenanceEventListingActions.showOkDialog),
+ tap((request) => {
+ this.dialog.open(OkDialog, {
+ data: {
+ title: request.title,
+ message: request.message
+ },
+ panelClass: 'medium-dialog'
+ });
+ })
+ ),
+ { dispatch: false }
+ );
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.reducer.ts
new file mode 100644
index 0000000000..c32db2c47d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.reducer.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 { createReducer, on } from '@ngrx/store';
+import { ProvenanceEventListingState } from './index';
+import {
+ clearProvenanceRequest,
+ loadProvenanceOptionsSuccess,
+ pollProvenanceQuerySuccess,
+ provenanceApiError,
+ saveProvenanceRequest,
+ submitProvenanceQuery,
+ submitProvenanceQuerySuccess
+} from './provenance-event-listing.actions';
+
+export const initialState: ProvenanceEventListingState = {
+ options: null,
+ request: null,
+ provenance: null,
+ saving: false,
+ loadedTimestamp: '',
+ error: null,
+ status: 'pending'
+};
+
+export const provenanceEventListingReducer = createReducer(
+ initialState,
+ on(loadProvenanceOptionsSuccess, (state, { response }) => ({
+ ...state,
+ options: response.provenanceOptions
+ })),
+ on(submitProvenanceQuery, (state) => ({
+ ...state,
+ status: 'loading' as const
+ })),
+ on(submitProvenanceQuerySuccess, pollProvenanceQuerySuccess, (state, { response }) => ({
+ ...state,
+ provenance: response.provenance,
+ loadedTimestamp: response.provenance.results.generated,
+ error: null,
+ status: 'success' as const
+ })),
+ on(saveProvenanceRequest, (state, { request }) => ({
+ ...state,
+ request
+ })),
+ on(clearProvenanceRequest, (state) => ({
+ ...state,
+ request: null
+ })),
+ on(provenanceApiError, (state, { error }) => ({
+ ...state,
+ saving: false,
+ error,
+ status: 'error' as const
+ }))
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.selectors.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.selectors.ts
new file mode 100644
index 0000000000..8225e4ce9f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/state/provenance-event-listing/provenance-event-listing.selectors.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { createSelector } from '@ngrx/store';
+import { ProvenanceState, selectProvenanceState } from '../index';
+import {
+ Provenance,
+ provenanceEventListingFeatureKey,
+ ProvenanceEventListingState,
+ ProvenanceQueryParams,
+ ProvenanceRequest,
+ ProvenanceResults
+} from './index';
+import { selectCurrentRoute } from '../../../../state/router/router.selectors';
+
+export const selectProvenanceEventListingState = createSelector(
+ selectProvenanceState,
+ (state: ProvenanceState) => state[provenanceEventListingFeatureKey]
+);
+
+export const selectSearchableFieldsFromRoute = createSelector(selectCurrentRoute, (route) => {
+ if (route) {
+ const queryParameters: ProvenanceQueryParams = {};
+ if (route.queryParams.componentId) {
+ queryParameters.componentId = route.queryParams.componentId;
+ }
+ if (route.queryParams.flowFileUuid) {
+ queryParameters.flowFileUuid = route.queryParams.flowFileUuid;
+ }
+ return queryParameters;
+ }
+ return null;
+});
+
+export const selectProvenanceRequest = createSelector(
+ selectProvenanceEventListingState,
+ (state: ProvenanceEventListingState) => state.request
+);
+
+export const selectStatus = createSelector(
+ selectProvenanceEventListingState,
+ (state: ProvenanceEventListingState) => state.status
+);
+
+export const selectProvenanceOptions = createSelector(
+ selectProvenanceEventListingState,
+ (state: ProvenanceEventListingState) => state.options
+);
+
+export const selectLoadedTimestamp = createSelector(
+ selectProvenanceEventListingState,
+ (state: ProvenanceEventListingState) => state.loadedTimestamp
+);
+
+export const selectProvenance = createSelector(
+ selectProvenanceEventListingState,
+ (state: ProvenanceEventListingState) => state.provenance
+);
+
+export const selectProvenanceId = createSelector(selectProvenance, (state: Provenance | null) => state?.id);
+
+export const selectClusterNodeId = createSelector(
+ selectProvenanceRequest,
+ (state: ProvenanceRequest | null) => state?.clusterNodeId
+);
+
+export const selectProvenanceResults = createSelector(selectProvenance, (state: Provenance | null) => state?.results);
+
+export const selectTimeOffset = createSelector(
+ selectProvenanceResults,
+ (state: ProvenanceResults | undefined) => state?.timeOffset
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing-routing.module.ts
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing-routing.module.ts
index 3789433cd3..88da6f535d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing-routing.module.ts
@@ -15,21 +15,19 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ProvenanceEventListing } from './provenance-event-listing.component';
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+const routes: Routes = [
+ {
+ path: '',
+ component: ProvenanceEventListing
+ }
+];
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class ProvenanceEventListingRoutingModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.html
new file mode 100644
index 0000000000..748cbe30c9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.html
@@ -0,0 +1,43 @@
+<!--
+ ~ 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.
+ -->
+
+<div class="flex flex-col h-full gap-y-2 text-sm" *ngIf="status$ | async; let status">
+ <div class="flex-1">
+ <ng-container *ngIf="provenance$ | async as provenance; else initialLoading">
+ <provenance-event-table
+ [events]="provenance.results.provenanceEvents"
+ [oldestEventAvailable]="provenance.results.oldestEvent"
+ [resultsMessage]="getResultsMessage(provenance)"
+ [hasRequest]="hasRequest(provenance.request)"
+ (openSearchCriteria)="openSearchCriteria()"
+ (openEventDialog)="openEventDialog($event)"
+ (clearRequest)="clearRequest()"></provenance-event-table>
+ </ng-container>
+ <ng-template #initialLoading>
+ <ngx-skeleton-loader count="3"></ngx-skeleton-loader>
+ </ng-template>
+ </div>
+ <div class="flex justify-between">
+ <div class="refresh-container flex items-center gap-x-2">
+ <button class="nifi-button" (click)="refreshParameterContextListing()">
+ <i class="fa fa-refresh" [class.fa-spin]="(status$ | async) === 'loading'"></i>
+ </button>
+ <div>Last updated:</div>
+ <div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
+ </div>
+ </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.scss
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.scss
index 3789433cd3..2944f98194 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.scss
@@ -14,22 +14,3 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
-
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
-}
-
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.spec.ts
new file mode 100644
index 0000000000..6b6b48eee5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.spec.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ProvenanceEventListing } from './provenance-event-listing.component';
+import { provideMockStore } from '@ngrx/store/testing';
+import { initialState } from '../../state/provenance-event-listing/provenance-event-listing.reducer';
+
+describe('ParameterContextListing', () => {
+ let component: ProvenanceEventListing;
+ let fixture: ComponentFixture<ProvenanceEventListing>;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [ProvenanceEventListing],
+ providers: [provideMockStore({ initialState })]
+ });
+ fixture = TestBed.createComponent(ProvenanceEventListing);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.ts
new file mode 100644
index 0000000000..075a8f0b77
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.component.ts
@@ -0,0 +1,177 @@
+/*
+ * 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 } from '@angular/core';
+import { Store } from '@ngrx/store';
+import {
+ Provenance,
+ ProvenanceEventListingState,
+ ProvenanceRequest,
+ ProvenanceResults
+} from '../../state/provenance-event-listing';
+import {
+ selectLoadedTimestamp,
+ selectProvenance,
+ selectProvenanceRequest,
+ selectSearchableFieldsFromRoute,
+ selectStatus
+} from '../../state/provenance-event-listing/provenance-event-listing.selectors';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { filter, map, take, tap } from 'rxjs';
+import {
+ clearProvenanceRequest,
+ openProvenanceEventDialog,
+ openSearchDialog,
+ resubmitProvenanceQuery,
+ saveProvenanceRequest
+} from '../../state/provenance-event-listing/provenance-event-listing.actions';
+import { ProvenanceSearchDialog } from './provenance-search-dialog/provenance-search-dialog.component';
+import { ProvenanceEventSummary } from '../../../../state/shared';
+
+@Component({
+ selector: 'provenance-event-listing',
+ templateUrl: './provenance-event-listing.component.html',
+ styleUrls: ['./provenance-event-listing.component.scss']
+})
+export class ProvenanceEventListing {
+ status$ = this.store.select(selectStatus);
+ loadedTimestamp$ = this.store.select(selectLoadedTimestamp);
+ provenance$ = this.store.select(selectProvenance);
+
+ request!: ProvenanceRequest;
+
+ constructor(private store: Store<ProvenanceEventListingState>) {
+ this.store
+ .select(selectSearchableFieldsFromRoute)
+ .pipe(
+ filter((queryParams) => queryParams != null),
+ // only consider the first searchable fields from route, subsequent changes to the route will be present in the saved request
+ take(1),
+ map((queryParams) => {
+ const request: ProvenanceRequest = {
+ incrementalResults: false,
+ maxResults: 1000,
+ summarize: true
+ };
+
+ if (queryParams) {
+ if (queryParams.componentId || queryParams.flowFileUuid) {
+ request.searchTerms = {};
+
+ if (queryParams.componentId) {
+ request.searchTerms['ProcessorID'] = {
+ value: queryParams.componentId,
+ inverse: false
+ };
+ }
+ if (queryParams.flowFileUuid) {
+ request.searchTerms['FlowFileUUID'] = {
+ value: queryParams.flowFileUuid,
+ inverse: false
+ };
+ }
+ }
+ }
+
+ return request;
+ })
+ )
+ .subscribe((request) => {
+ this.store.dispatch(
+ saveProvenanceRequest({
+ request
+ })
+ );
+ });
+
+ // any changes to the saved request will trigger a provenance query submission
+ this.store
+ .select(selectProvenanceRequest)
+ .pipe(
+ map((request) => {
+ if (request) {
+ return request;
+ }
+
+ const initialRequest: ProvenanceRequest = {
+ incrementalResults: false,
+ maxResults: 1000,
+ summarize: true
+ };
+
+ return initialRequest;
+ }),
+ tap((request) => (this.request = request)),
+ takeUntilDestroyed()
+ )
+ .subscribe((request) => {
+ this.store.dispatch(
+ resubmitProvenanceQuery({
+ request
+ })
+ );
+ });
+ }
+
+ getResultsMessage(provenance: Provenance): string {
+ const request: ProvenanceRequest = provenance.request;
+ const results: ProvenanceResults = provenance.results;
+
+ if (this.hasRequest(request)) {
+ if (results.totalCount >= ProvenanceSearchDialog.MAX_RESULTS) {
+ return `Showing ${ProvenanceSearchDialog.MAX_RESULTS} of ${results.totalCount} events that match the specified query, please refine the search.`;
+ } else {
+ return 'Showing the events that match the specified query.';
+ }
+ } else {
+ if (results.totalCount >= ProvenanceSearchDialog.MAX_RESULTS) {
+ return `Showing the most recent ${ProvenanceSearchDialog.MAX_RESULTS} of ${results.totalCount} events, please refine the search.`;
+ } else {
+ return 'Showing the most recent events.';
+ }
+ }
+ }
+
+ hasRequest(request: ProvenanceRequest): boolean {
+ const hasSearchTerms: boolean = !!request.searchTerms && Object.keys(request.searchTerms).length > 0;
+ return !!request.startDate || !!request.endDate || hasSearchTerms;
+ }
+
+ clearRequest(): void {
+ this.store.dispatch(clearProvenanceRequest());
+ }
+
+ openSearchCriteria(): void {
+ this.store.dispatch(openSearchDialog());
+ }
+
+ openEventDialog(event: ProvenanceEventSummary): void {
+ this.store.dispatch(
+ openProvenanceEventDialog({
+ id: event.id
+ })
+ );
+ }
+
+ refreshParameterContextListing(): void {
+ this.store.dispatch(
+ resubmitProvenanceQuery({
+ request: this.request
+ })
+ );
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.module.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.module.ts
new file mode 100644
index 0000000000..a0895569ff
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-listing.module.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ProvenanceEventListing } from './provenance-event-listing.component';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+import { MatSortModule } from '@angular/material/sort';
+import { MatTableModule } from '@angular/material/table';
+import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
+import { ProvenanceEventListingRoutingModule } from './provenance-event-listing-routing.module';
+import { ProvenanceEventTable } from './provenance-event-table/provenance-event-table.component';
+
+@NgModule({
+ declarations: [ProvenanceEventListing],
+ exports: [ProvenanceEventListing],
+ imports: [
+ CommonModule,
+ ProvenanceEventListingRoutingModule,
+ NgxSkeletonLoaderModule,
+ MatSortModule,
+ MatTableModule,
+ NifiTooltipDirective,
+ ProvenanceEventTable
+ ]
+})
+export class ProvenanceEventListingModule {}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.html
new file mode 100644
index 0000000000..abea7075c8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.html
@@ -0,0 +1,156 @@
+<!--
+ ~ 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.
+ -->
+
+<div class="provenance-event-table h-full flex flex-col">
+ <div class="flex flex-col">
+ <div class="value font-bold">Displaying {{ filteredCount }} of {{ totalCount }}</div>
+ <div class="flex justify-between">
+ <div>
+ Oldest event available: <span class="value">{{ oldestEventAvailable }}</span>
+ </div>
+ <div>
+ {{ resultsMessage }}
+ <a *ngIf="hasRequest" (click)="clearRequestClicked()">Clear Search</a>
+ </div>
+ </div>
+ <div class="flex justify-between">
+ <form [formGroup]="filterForm">
+ <div class="flex pt-2">
+ <div class="mr-2">
+ <mat-form-field>
+ <mat-label>Filter</mat-label>
+ <input matInput type="text" class="small" formControlName="filterTerm" />
+ </mat-form-field>
+ </div>
+ <div>
+ <mat-form-field>
+ <mat-label>Filter By</mat-label>
+ <mat-select formControlName="filterColumn">
+ <mat-option *ngFor="let option of filterColumnOptions" [value]="option"
+ >{{ option }}
+ </mat-option>
+ </mat-select>
+ </mat-form-field>
+ </div>
+ </div>
+ </form>
+ <div class="flex flex-col justify-center">
+ <button class="nifi-button" (click)="searchClicked()">
+ <i class="fa fa-search"></i>
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="flex-1 relative">
+ <div class="listing-table border absolute inset-0 overflow-y-auto">
+ <table
+ mat-table
+ [dataSource]="dataSource"
+ matSort
+ matSortDisableClear
+ (matSortChange)="updateSort($event)"
+ [matSortActive]="sort.active"
+ [matSortDirection]="sort.direction">
+ <!-- More Details Column -->
+ <ng-container matColumnDef="moreDetails">
+ <th mat-header-cell *matHeaderCellDef></th>
+ <td mat-cell *matCellDef="let item">
+ <div
+ class="pointer fa fa-info-circle"
+ title="View Details"
+ (click)="viewDetailsClicked(item)"></div>
+ </td>
+ </ng-container>
+
+ <!-- Name Column -->
+ <ng-container matColumnDef="eventTime">
+ <th mat-header-cell *matHeaderCellDef mat-sort-header>Event Time</th>
+ <td mat-cell *matCellDef="let item">
+ {{ item.eventTime }}
+ </td>
+ </ng-container>
+
+ <!-- Type Column -->
+ <ng-container matColumnDef="eventType">
+ <th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th>
+ <td mat-cell *matCellDef="let item">
+ {{ item.eventType }}
+ </td>
+ </ng-container>
+
+ <!-- FlowFile UUID Column -->
+ <ng-container matColumnDef="flowFileUuid">
+ <th mat-header-cell *matHeaderCellDef mat-sort-header>FlowFile UUID</th>
+ <td mat-cell *matCellDef="let item">
+ {{ item.flowFileUuid }}
+ </td>
+ </ng-container>
+
+ <!-- File Size Column -->
+ <ng-container matColumnDef="fileSize">
+ <th mat-header-cell *matHeaderCellDef mat-sort-header>File Size</th>
+ <td mat-cell *matCellDef="let item">
+ {{ item.fileSize }}
+ </td>
+ </ng-container>
+
+ <!-- Component Name Column -->
+ <ng-container matColumnDef="componentName">
+ <th mat-header-cell *matHeaderCellDef mat-sort-header>Component Name</th>
+ <td mat-cell *matCellDef="let item">
+ {{ item.componentName }}
+ </td>
+ </ng-container>
+
+ <!-- Component Type Column -->
+ <ng-container matColumnDef="componentType">
+ <th mat-header-cell *matHeaderCellDef mat-sort-header>Component Type</th>
+ <td mat-cell *matCellDef="let item">
+ {{ item.componentType }}
+ </td>
+ </ng-container>
+
+ <!-- Actions Column -->
+ <ng-container matColumnDef="actions">
+ <th mat-header-cell *matHeaderCellDef></th>
+ <td mat-cell *matCellDef="let item">
+ <div class="flex items-center gap-x-3">
+ <!-- <div-->
+ <!-- class="pointer icon icon-lineage"-->
+ <!-- *ngIf="canStop(item)"-->
+ <!-- (click)="stopClicked(item)"-->
+ <!-- title="Stop"></div>-->
+ <div
+ *ngIf="supportsGoTo(item)"
+ class="pointer fa fa-long-arrow-right"
+ title="Go To"
+ [routerLink]="getComponentLink(item)"></div>
+ </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)"
+ [class.selected]="isSelected(row)"
+ [class.even]="even"></tr>
+ </table>
+ </div>
+ </div>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.scss
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.scss
index 3789433cd3..d95e7e66fb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.scss
@@ -15,21 +15,13 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
-
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
+.provenance-event-table {
+ .listing-table {
+ table {
+ .mat-column-actions {
+ min-width: 50px;
+ width: 50px;
+ }
+ }
+ }
}
-
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.spec.ts
new file mode 100644
index 0000000000..497c2eb142
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.spec.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ProvenanceEventTable } from './provenance-event-table.component';
+import { MatTableModule } from '@angular/material/table';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('ProvenanceEventTable', () => {
+ let component: ProvenanceEventTable;
+ let fixture: ComponentFixture<ProvenanceEventTable>;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ProvenanceEventTable, MatTableModule, BrowserAnimationsModule]
+ });
+ fixture = TestBed.createComponent(ProvenanceEventTable);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.ts
new file mode 100644
index 0000000000..d80c6f6d03
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/provenance-event-table.component.ts
@@ -0,0 +1,232 @@
+/*
+ * 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, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { MatSortModule, Sort } from '@angular/material/sort';
+import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
+import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
+import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
+import { NiFiCommon } from '../../../../../service/nifi-common.service';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatOptionModule } from '@angular/material/core';
+import { MatSelectModule } from '@angular/material/select';
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { NgForOf, NgIf } from '@angular/common';
+import { debounceTime } from 'rxjs';
+import { ProvenanceEventSummary } from '../../../../../state/shared';
+import { RouterLink } from '@angular/router';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
+
+@Component({
+ selector: 'provenance-event-table',
+ standalone: true,
+ templateUrl: './provenance-event-table.component.html',
+ imports: [
+ MatTableModule,
+ MatSortModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatOptionModule,
+ MatSelectModule,
+ ReactiveFormsModule,
+ NgForOf,
+ NgIf,
+ RouterLink,
+ NgxSkeletonLoaderModule
+ ],
+ styleUrls: ['./provenance-event-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
+})
+export class ProvenanceEventTable implements AfterViewInit {
+ @Input() set events(events: ProvenanceEventSummary[]) {
+ if (events) {
+ this.dataSource.data = this.sortEvents(events, this.sort);
+ this.dataSource.filterPredicate = (data: ProvenanceEventSummary, filter: string) => {
+ const filterArray = filter.split('|');
+ const filterTerm = filterArray[0];
+ const filterColumn = filterArray[1];
+
+ if (filterColumn === this.filterColumnOptions[0]) {
+ return data.componentName.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
+ } else if (filterColumn === this.filterColumnOptions[1]) {
+ return data.componentType.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
+ } else {
+ return data.eventType.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
+ }
+ };
+ this.totalCount = events.length;
+ this.filteredCount = events.length;
+
+ // apply any filtering to the new data
+ const filterTerm = this.filterForm.get('filterTerm')?.value;
+ if (filterTerm?.length > 0) {
+ const filterColumn = this.filterForm.get('filterColumn')?.value;
+ this.applyFilter(filterTerm, filterColumn);
+ }
+ }
+ }
+ @Input() oldestEventAvailable!: string;
+ @Input() resultsMessage!: string;
+ @Input() hasRequest!: boolean;
+
+ @Output() openSearchCriteria: EventEmitter<void> = new EventEmitter<void>();
+ @Output() clearRequest: EventEmitter<void> = new EventEmitter<void>();
+ @Output() openEventDialog: EventEmitter<ProvenanceEventSummary> = new EventEmitter<ProvenanceEventSummary>();
+
+ protected readonly TextTip = TextTip;
+ protected readonly BulletinsTip = BulletinsTip;
+ protected readonly ValidationErrorsTip = ValidationErrorsTip;
+
+ // TODO - conditionally include the cluster column
+ displayedColumns: string[] = [
+ 'moreDetails',
+ 'eventTime',
+ 'eventType',
+ 'flowFileUuid',
+ 'fileSize',
+ 'componentName',
+ 'componentType',
+ 'actions'
+ ];
+ dataSource: MatTableDataSource<ProvenanceEventSummary> = new MatTableDataSource<ProvenanceEventSummary>();
+ selectedEventId: string | null = null;
+
+ sort: Sort = {
+ active: 'eventTime',
+ direction: 'desc'
+ };
+
+ filterForm: FormGroup;
+ filterColumnOptions: string[] = ['component name', 'component type', 'type'];
+ totalCount: number = 0;
+ filteredCount: number = 0;
+
+ constructor(private formBuilder: FormBuilder) {
+ this.filterForm = this.formBuilder.group({ filterTerm: '', filterColumn: this.filterColumnOptions[0] });
+ }
+
+ ngAfterViewInit(): void {
+ this.filterForm
+ .get('filterTerm')
+ ?.valueChanges.pipe(debounceTime(500))
+ .subscribe((filterTerm: string) => {
+ const filterColumn = this.filterForm.get('filterColumn')?.value;
+ this.applyFilter(filterTerm, filterColumn);
+ });
+
+ this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
+ const filterTerm = this.filterForm.get('filterTerm')?.value;
+ this.applyFilter(filterTerm, filterColumn);
+ });
+ }
+
+ updateSort(sort: Sort): void {
+ this.sort = sort;
+ this.dataSource.data = this.sortEvents(this.dataSource.data, sort);
+ }
+
+ sortEvents(events: ProvenanceEventSummary[], sort: Sort): ProvenanceEventSummary[] {
+ const data: ProvenanceEventSummary[] = events.slice();
+ return data.sort((a, b) => {
+ const isAsc = sort.direction === 'asc';
+
+ switch (sort.active) {
+ case 'eventTime':
+ // event ideas are increasing, so we can use this simple number for sorting purposes
+ // since we don't surface the timestamp as millis
+ return (a.eventId - b.eventId) * (isAsc ? 1 : -1);
+ case 'eventType':
+ return this.compare(a.eventType, b.eventType, isAsc);
+ case 'flowFileUuid':
+ return this.compare(a.flowFileUuid, b.flowFileUuid, isAsc);
+ case 'fileSize':
+ return (a.fileSizeBytes - b.fileSizeBytes) * (isAsc ? 1 : -1);
+ case 'componentName':
+ return this.compare(a.componentName, b.componentName, isAsc);
+ case 'componentType':
+ return this.compare(a.componentType, b.componentType, isAsc);
+ default:
+ return 0;
+ }
+ });
+ }
+
+ private compare(a: string, b: string, isAsc: boolean): number {
+ if (a === b) {
+ return 0;
+ }
+
+ return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
+ }
+
+ applyFilter(filterTerm: string, filterColumn: string) {
+ this.dataSource.filter = `${filterTerm}|${filterColumn}`;
+ this.filteredCount = this.dataSource.filteredData.length;
+ }
+
+ clearRequestClicked(): void {
+ this.clearRequest.next();
+ }
+
+ searchClicked() {
+ this.openSearchCriteria.next();
+ }
+
+ viewDetailsClicked(event: ProvenanceEventSummary) {
+ this.openEventDialog.next(event);
+ }
+
+ select(event: ProvenanceEventSummary): void {
+ this.selectedEventId = event.id;
+ }
+
+ isSelected(event: ProvenanceEventSummary): boolean {
+ if (this.selectedEventId) {
+ return event.id == this.selectedEventId;
+ }
+ return false;
+ }
+
+ supportsGoTo(event: ProvenanceEventSummary): boolean {
+ if (event.groupId == null) {
+ return false;
+ }
+
+ if (event.componentId === 'Remote Output Port' || event.componentId === 'Remote Input Port') {
+ return false;
+ }
+
+ return true;
+ }
+
+ getComponentLink(event: ProvenanceEventSummary): string[] {
+ let link: string[];
+
+ if (event.groupId == event.componentId) {
+ link = ['/process-groups', event.componentId];
+ } else if (event.componentId === 'Connection' || event.componentId === 'Load Balanced Connection') {
+ link = ['/process-groups', event.groupId, 'Connection', event.componentId];
+ } else if (event.componentId === 'Output Port') {
+ link = ['/process-groups', event.groupId, 'OutputPort', event.componentId];
+ } else {
+ link = ['/process-groups', event.groupId, 'Processor', event.componentId];
+ }
+
+ return link;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.html
new file mode 100644
index 0000000000..086f0cb37a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.html
@@ -0,0 +1,76 @@
+<!--
+ ~ 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>Search Events</h2>
+<form class="search-events-form" [formGroup]="provenanceOptionsForm">
+ <mat-dialog-content>
+ <ng-container *ngFor="let searchableField of request.options.searchableFields">
+ <div [formGroupName]="searchableField.id">
+ <mat-form-field>
+ <mat-label>{{ searchableField.label }}</mat-label>
+ <input matInput formControlName="value" type="text" />
+ </mat-form-field>
+ <div class="-mt-6 mb-4">
+ <mat-checkbox formControlName="inverse" name="inverse"> Exclude </mat-checkbox>
+ </div>
+ </div>
+ </ng-container>
+ <mat-form-field>
+ <mat-label>Date Range</mat-label>
+ <mat-date-range-input [rangePicker]="picker">
+ <input matStartDate formControlName="startDate" placeholder="Start date" />
+ <input matEndDate formControlName="endDate" placeholder="End date" />
+ </mat-date-range-input>
+ <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
+ <mat-date-range-picker #picker></mat-date-range-picker>
+ </mat-form-field>
+ <div class="flex gap-x-3">
+ <div class="w-full flex flex-col">
+ <mat-form-field>
+ <mat-label>Start Time ({{ timezone }})</mat-label>
+ <input matInput type="text" formControlName="startTime" />
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>Minimum File Size</mat-label>
+ <input matInput type="text" formControlName="minFileSize" />
+ </mat-form-field>
+ </div>
+ <div class="w-full flex flex-col">
+ <mat-form-field>
+ <mat-label>End Time ({{ timezone }})</mat-label>
+ <input matInput type="text" formControlName="endTime" />
+ </mat-form-field>
+ <mat-form-field>
+ <mat-label>Maximum File Size</mat-label>
+ <input matInput type="text" formControlName="maxFileSize" />
+ </mat-form-field>
+ </div>
+ </div>
+ </mat-dialog-content>
+ <mat-dialog-actions align="end">
+ <button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
+ <button
+ [disabled]="provenanceOptionsForm.invalid"
+ type="button"
+ color="primary"
+ (click)="searchClicked()"
+ mat-raised-button
+ mat-dialog-close>
+ <span>Search</span>
+ </button>
+ </mat-dialog-actions>
+</form>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.scss
similarity index 53%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.scss
index 3789433cd3..219a472003 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.scss
@@ -15,21 +15,16 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+@use '@angular/material' as mat;
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+.search-events-form {
+ @include mat.button-density(-1);
+
+ .mat-mdc-form-field {
+ width: 100%;
+ }
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
+ .mdc-text-field__input::-webkit-calendar-picker-indicator {
+ display: block !important;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.spec.ts
new file mode 100644
index 0000000000..881177b7e9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.spec.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ProvenanceSearchDialog } from './provenance-search-dialog.component';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { MatNativeDateModule } from '@angular/material/core';
+
+describe('ProvenanceSearchDialog', () => {
+ let component: ProvenanceSearchDialog;
+ let fixture: ComponentFixture<ProvenanceSearchDialog>;
+
+ const data: any = {
+ timeOffset: -18000000,
+ options: {
+ searchableFields: [
+ {
+ id: 'EventType',
+ field: 'eventType',
+ label: 'Event Type',
+ type: 'STRING'
+ },
+ {
+ id: 'FlowFileUUID',
+ field: 'uuid',
+ label: 'FlowFile UUID',
+ type: 'STRING'
+ },
+ {
+ id: 'Filename',
+ field: 'filename',
+ label: 'Filename',
+ type: 'STRING'
+ },
+ {
+ id: 'ProcessorID',
+ field: 'processorId',
+ label: 'Component ID',
+ type: 'STRING'
+ },
+ {
+ id: 'Relationship',
+ field: 'relationship',
+ label: 'Relationship',
+ type: 'STRING'
+ }
+ ]
+ },
+ currentRequest: {
+ incrementalResults: false,
+ maxResults: 1000,
+ summarize: true
+ }
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ProvenanceSearchDialog, BrowserAnimationsModule, MatNativeDateModule],
+ providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
+ });
+ fixture = TestBed.createComponent(ProvenanceSearchDialog);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.ts
new file mode 100644
index 0000000000..6474387adb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component.ts
@@ -0,0 +1,220 @@
+/*
+ * 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, EventEmitter, Inject, Input, Output } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { MatInputModule } from '@angular/material/input';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatButtonModule } from '@angular/material/button';
+import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
+import {
+ ProvenanceRequest,
+ ProvenanceSearchDialogRequest,
+ SearchableField
+} from '../../../state/provenance-event-listing';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { NiFiCommon } from '../../../../../service/nifi-common.service';
+
+@Component({
+ selector: 'provenance-search-dialog',
+ standalone: true,
+ templateUrl: './provenance-search-dialog.component.html',
+ imports: [
+ ReactiveFormsModule,
+ MatDialogModule,
+ MatInputModule,
+ MatCheckboxModule,
+ MatButtonModule,
+ NgIf,
+ AsyncPipe,
+ NgForOf,
+ MatDatepickerModule
+ ],
+ styleUrls: ['./provenance-search-dialog.component.scss']
+})
+export class ProvenanceSearchDialog {
+ @Input() timezone!: string;
+ @Output() submitSearchCriteria: EventEmitter<ProvenanceRequest> = new EventEmitter<ProvenanceRequest>();
+
+ public static readonly MAX_RESULTS: number = 1000;
+ private static readonly DEFAULT_START_TIME: string = '00:00:00';
+ private static readonly DEFAULT_END_TIME: string = '23:59:59';
+
+ provenanceOptionsForm: FormGroup;
+
+ constructor(
+ @Inject(MAT_DIALOG_DATA) public request: ProvenanceSearchDialogRequest,
+ private formBuilder: FormBuilder,
+ private nifiCommon: NiFiCommon
+ ) {
+ const now = new Date();
+ this.clearTime(now);
+
+ let startDate: Date = now;
+ let startTime: string = ProvenanceSearchDialog.DEFAULT_START_TIME;
+ let endDate: Date = now;
+ let endTime: string = ProvenanceSearchDialog.DEFAULT_END_TIME;
+ let minFileSize: string = '';
+ let maxFileSize: string = '';
+
+ if (request.currentRequest) {
+ const requestedStartDate = request.currentRequest.startDate;
+ if (requestedStartDate) {
+ startDate = this.nifiCommon.parseDateTime(requestedStartDate);
+ this.clearTime(startDate);
+
+ const requestedStartDateTime = requestedStartDate?.split(' ');
+ if (requestedStartDateTime && requestedStartDateTime.length > 1) {
+ startTime = requestedStartDateTime[1];
+ }
+ }
+
+ const requestedEndDate = request.currentRequest.endDate;
+ if (requestedEndDate) {
+ endDate = this.nifiCommon.parseDateTime(requestedEndDate);
+ this.clearTime(endDate);
+
+ const requestedEndDateTime = requestedEndDate?.split(' ');
+ if (requestedEndDateTime && requestedEndDateTime.length > 0) {
+ endTime = requestedEndDateTime[1];
+ }
+ }
+
+ if (request.currentRequest.minimumFileSize) {
+ minFileSize = request.currentRequest.minimumFileSize;
+ }
+
+ if (request.currentRequest.maximumFileSize) {
+ maxFileSize = request.currentRequest.maximumFileSize;
+ }
+ }
+
+ this.provenanceOptionsForm = this.formBuilder.group({
+ startDate: new FormControl(startDate),
+ startTime: new FormControl(startTime),
+ endDate: new FormControl(endDate),
+ endTime: new FormControl(endTime),
+ minFileSize: new FormControl(minFileSize),
+ maxFileSize: new FormControl(maxFileSize)
+ });
+
+ const searchTerms: any = request.currentRequest?.searchTerms;
+
+ const searchableFields: SearchableField[] = request.options.searchableFields;
+ searchableFields.forEach((searchableField) => {
+ let value: string = '';
+ let inverse: boolean = false;
+ if (searchTerms && searchTerms[searchableField.id]) {
+ value = searchTerms[searchableField.id].value;
+ inverse = searchTerms[searchableField.id].inverse;
+ }
+
+ this.provenanceOptionsForm.addControl(
+ searchableField.id,
+ this.formBuilder.group({
+ value: new FormControl(value),
+ inverse: new FormControl(inverse)
+ })
+ );
+ });
+ }
+
+ private clearTime(date: Date): void {
+ date.setHours(0);
+ date.setMinutes(0);
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ }
+
+ searchClicked() {
+ const provenanceRequest: ProvenanceRequest = {
+ maxResults: ProvenanceSearchDialog.MAX_RESULTS,
+ summarize: true,
+ incrementalResults: false
+ };
+
+ const startDate: Date = this.provenanceOptionsForm.get('startDate')?.value;
+ if (startDate) {
+ // convert the date object into the format the api expects
+ const formatted: string = this.nifiCommon.formatDateTime(startDate);
+
+ // get just the date portion because the time is entered separately by the user
+ const formattedStartDateTime = formatted.split(' ');
+ if (formattedStartDateTime.length > 0) {
+ const formattedStartDate = formattedStartDateTime[0];
+
+ let startTime: string = this.provenanceOptionsForm.get('startTime')?.value;
+ if (!startTime) {
+ startTime = ProvenanceSearchDialog.DEFAULT_START_TIME;
+ }
+
+ // combine all three pieces into the format the api requires
+ provenanceRequest.startDate = `${formattedStartDate} ${startTime} ${this.timezone}`;
+ }
+ }
+
+ const endDate: Date = this.provenanceOptionsForm.get('endDate')?.value;
+ if (endDate) {
+ // convert the date object into the format the api expects
+ const formatted: string = this.nifiCommon.formatDateTime(endDate);
+
+ // get just the date portion because the time is entered separately by the user
+ const formattedEndDateTime = formatted.split(' ');
+ if (formattedEndDateTime.length > 0) {
+ const formattedEndDate = formattedEndDateTime[0];
+
+ let endTime: string = this.provenanceOptionsForm.get('endTime')?.value;
+ if (!endTime) {
+ endTime = ProvenanceSearchDialog.DEFAULT_END_TIME;
+ }
+
+ // combine all three pieces into the format the api requires
+ provenanceRequest.endDate = `${formattedEndDate} ${endTime} ${this.timezone}`;
+ }
+ }
+
+ const minFileSize: string = this.provenanceOptionsForm.get('minFileSize')?.value;
+ if (minFileSize) {
+ provenanceRequest.minimumFileSize = minFileSize;
+ }
+
+ const maxFileSize: string = this.provenanceOptionsForm.get('maxFileSize')?.value;
+ if (maxFileSize) {
+ provenanceRequest.maximumFileSize = maxFileSize;
+ }
+
+ const searchTerms: any = {};
+ const searchableFields: SearchableField[] = this.request.options.searchableFields;
+ searchableFields.forEach((searchableField) => {
+ // @ts-ignore
+ const searchableFieldForm: FormGroup = this.provenanceOptionsForm.get(searchableField.id);
+ if (searchableFieldForm) {
+ const searchableFieldValue: string = searchableFieldForm.get('value')?.value;
+ if (searchableFieldValue) {
+ searchTerms[searchableField.id] = {
+ value: searchableFieldValue,
+ inverse: searchableFieldForm.get('inverse')?.value
+ };
+ }
+ }
+ });
+ provenanceRequest.searchTerms = searchTerms;
+
+ this.submitSearchCriteria.next(provenanceRequest);
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/about.service.ts
similarity index 53%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/about.service.ts
index 3789433cd3..af8c75c85b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/about.service.ts
@@ -15,21 +15,17 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { Injectable } from '@angular/core';
+import { Observable, throwError } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+@Injectable({ providedIn: 'root' })
+export class AboutService {
+ private static readonly API: string = '../nifi-api';
+
+ constructor(private httpClient: HttpClient) {}
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
+ getAbout(): Observable<any> {
+ return this.httpClient.get(`${AboutService.API}/flow/about`);
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts
index c09b87055d..6d2eee70c8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/service/nifi-common.service.ts
@@ -21,6 +21,11 @@ import { Injectable } from '@angular/core';
providedIn: 'root'
})
export class NiFiCommon {
+ private static readonly MILLIS_PER_DAY: number = 86400000;
+ private static readonly MILLIS_PER_HOUR: number = 3600000;
+ private static readonly MILLIS_PER_MINUTE: number = 60000;
+ private static readonly MILLIS_PER_SECOND: number = 1000;
+
constructor() {}
/**
@@ -171,4 +176,144 @@ export class NiFiCommon {
}
return NiFiCommon.LEAD_TRAIL_WHITE_SPACE_REGEX.test(value);
}
+
+ /**
+ * Pads the specified value to the specified width with the specified character.
+ * If the specified value is already wider than the specified width, the original
+ * value is returned.
+ *
+ * @param {integer} value
+ * @param {integer} width
+ * @param {string} character
+ * @returns {string}
+ */
+ pad(value: number, width: number, character: string): string {
+ let s: string = value + '';
+
+ // pad until wide enough
+ while (s.length < width) {
+ s = character + s;
+ }
+
+ return s;
+ }
+
+ /**
+ * Formats the specified DateTime.
+ *
+ * @param {Date} date
+ * @returns {String}
+ */
+ formatDateTime(date: Date): string {
+ return (
+ this.pad(date.getMonth() + 1, 2, '0') +
+ '/' +
+ this.pad(date.getDate(), 2, '0') +
+ '/' +
+ this.pad(date.getFullYear(), 2, '0') +
+ ' ' +
+ this.pad(date.getHours(), 2, '0') +
+ ':' +
+ this.pad(date.getMinutes(), 2, '0') +
+ ':' +
+ this.pad(date.getSeconds(), 2, '0') +
+ '.' +
+ this.pad(date.getMilliseconds(), 3, '0')
+ );
+ }
+
+ /**
+ * Parses the specified date time into a Date object. The resulting
+ * object does not account for timezone and should only be used for
+ * performing relative comparisons.
+ *
+ * @param {string} rawDateTime
+ * @returns {Date}
+ */
+ parseDateTime(rawDateTime: string): Date {
+ // handle non date values
+ if (!rawDateTime) {
+ return new Date();
+ }
+
+ // parse the date time
+ const dateTime: string[] = rawDateTime.split(/ /);
+
+ // ensure the correct number of tokens
+ if (dateTime.length !== 3) {
+ return new Date();
+ }
+
+ // get the date and time
+ const date: string[] = dateTime[0].split(/\//);
+ const time: string[] = dateTime[1].split(/:/);
+
+ // ensure the correct number of tokens
+ if (date.length !== 3 || time.length !== 3) {
+ return new Date();
+ }
+ const year: number = parseInt(date[2], 10);
+ const month: number = parseInt(date[0], 10) - 1; // new Date() accepts months 0 - 11
+ const day: number = parseInt(date[1], 10);
+ const hours: number = parseInt(time[0], 10);
+ const minutes: number = parseInt(time[1], 10);
+
+ // detect if there is millis
+ const secondsSpec: string[] = time[2].split(/\./);
+ const seconds: number = parseInt(secondsSpec[0], 10);
+ let milliseconds: number = 0;
+ if (secondsSpec.length === 2) {
+ milliseconds = parseInt(secondsSpec[1], 10);
+ }
+ return new Date(year, month, day, hours, minutes, seconds, milliseconds);
+ }
+
+ /**
+ * Formats the specified duration.
+ *
+ * @param {number} millis in millis
+ */
+ formatDuration(millis: number): string {
+ // don't support sub millisecond resolution
+ let duration: number = millis < 1 ? 0 : millis;
+
+ // determine the number of days in the specified duration
+ let days: number = duration / NiFiCommon.MILLIS_PER_DAY;
+ days = days >= 1 ? Math.trunc(days) : 0;
+ duration %= NiFiCommon.MILLIS_PER_DAY;
+
+ // remaining duration should be less than 1 day, get number of hours
+ let hours: number = duration / NiFiCommon.MILLIS_PER_HOUR;
+ hours = hours >= 1 ? Math.trunc(hours) : 0;
+ duration %= NiFiCommon.MILLIS_PER_HOUR;
+
+ // remaining duration should be less than 1 hour, get number of minutes
+ let minutes: number = duration / NiFiCommon.MILLIS_PER_MINUTE;
+ minutes = minutes >= 1 ? Math.trunc(minutes) : 0;
+ duration %= NiFiCommon.MILLIS_PER_MINUTE;
+
+ // remaining duration should be less than 1 minute, get number of seconds
+ let seconds: number = duration / NiFiCommon.MILLIS_PER_SECOND;
+ seconds = seconds >= 1 ? Math.trunc(seconds) : 0;
+
+ // remaining duration is the number millis (don't support sub millisecond resolution)
+ duration = Math.floor(duration % NiFiCommon.MILLIS_PER_SECOND);
+
+ // format the time
+ const time =
+ this.pad(hours, 2, '0') +
+ ':' +
+ this.pad(minutes, 2, '0') +
+ ':' +
+ this.pad(seconds, 2, '0') +
+ '.' +
+ this.pad(duration, 3, '0');
+
+ // only include days if appropriate
+ if (days > 0) {
+ return days + ' days and ' + time;
+ } else {
+ return time;
+ }
+ }
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.actions.ts
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.actions.ts
index 3789433cd3..208427b30e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.actions.ts
@@ -15,21 +15,13 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { createAction, props } from '@ngrx/store';
+import { LoadAboutResponse } from './index';
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+export const loadAbout = createAction('[About] Load About');
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
+export const loadAboutSuccess = createAction('[About] Load About Success', props<{ response: LoadAboutResponse }>());
+
+export const aboutApiError = createAction('[About] About Api Error', props<{ error: string }>());
+
+export const clearAboutApiError = createAction('[User] Clear About Api Error');
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.effects.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.effects.ts
new file mode 100644
index 0000000000..6d6cb7835f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.effects.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { Injectable } from '@angular/core';
+import { Actions, createEffect, ofType } from '@ngrx/effects';
+import * as AboutActions from './about.actions';
+import { catchError, from, map, of, switchMap } from 'rxjs';
+import { AboutService } from '../../service/about.service';
+
+@Injectable()
+export class AboutEffects {
+ constructor(
+ private actions$: Actions,
+ private aboutService: AboutService
+ ) {}
+
+ loadAbout$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(AboutActions.loadAbout),
+ switchMap(() => {
+ return from(
+ this.aboutService.getAbout().pipe(
+ map((response) =>
+ AboutActions.loadAboutSuccess({
+ response: response
+ })
+ ),
+ catchError((error) => of(AboutActions.aboutApiError({ error: error.error })))
+ )
+ );
+ })
+ )
+ );
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.reducer.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.reducer.ts
new file mode 100644
index 0000000000..db98b4bb6d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.reducer.ts
@@ -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.
+ */
+
+import { createReducer, on } from '@ngrx/store';
+import { AboutState } from './index';
+import { aboutApiError, clearAboutApiError, loadAbout, loadAboutSuccess } from './about.actions';
+
+export const initialState: AboutState = {
+ about: null,
+ error: null,
+ status: 'pending'
+};
+
+export const aboutReducer = createReducer(
+ initialState,
+ on(loadAbout, (state) => ({
+ ...state,
+ status: 'loading' as const
+ })),
+ on(loadAboutSuccess, (state, { response }) => ({
+ ...state,
+ about: response.about,
+ error: null,
+ status: 'success' as const
+ })),
+ on(aboutApiError, (state, { error }) => ({
+ ...state,
+ error: error,
+ status: 'error' as const
+ })),
+ on(clearAboutApiError, (state) => ({
+ ...state,
+ error: null,
+ status: 'pending' as const
+ }))
+);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.selectors.ts
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.selectors.ts
index 3789433cd3..14fd41d990 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/about.selectors.ts
@@ -15,21 +15,9 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { createFeatureSelector, createSelector } from '@ngrx/store';
+import { aboutFeatureKey, AboutState } from './index';
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+export const selectAboutState = createFeatureSelector<AboutState>(aboutFeatureKey);
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
+export const selectAbout = createSelector(selectAboutState, (state: AboutState) => state.about);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/index.ts
similarity index 53%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/index.ts
index 3789433cd3..ed3f499c14 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/about/index.ts
@@ -15,21 +15,26 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+export const aboutFeatureKey = 'about';
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
+export interface LoadAboutResponse {
+ about: About;
}
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
+export interface About {
+ title: string;
+ version: string;
+ uri: string;
+ contentViewerUrl: string;
+ timezone: string;
+ buildTag: string;
+ buildRevision: string;
+ buildBranch: string;
+ buildTimestamp: string;
+}
+
+export interface AboutState {
+ about: About | null;
+ error: string | null;
+ status: 'pending' | 'loading' | 'error' | 'success';
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
index 3789433cd3..f943193426 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
@@ -21,15 +21,19 @@ import { UserState, userFeatureKey } from './user';
import { userReducer } from './user/user.reducer';
import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { aboutFeatureKey, AboutState } from './about';
+import { aboutReducer } from './about/about.reducer';
export interface NiFiState {
router: RouterReducerState;
[userFeatureKey]: UserState;
[extensionTypesFeatureKey]: ExtensionTypesState;
+ [aboutFeatureKey]: AboutState;
}
export const rootReducers: ActionReducerMap<NiFiState> = {
router: routerReducer,
[userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
+ [extensionTypesFeatureKey]: extensionTypesReducer,
+ [aboutFeatureKey]: aboutReducer
};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts
index a60037d12f..de07655740 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/shared/index.ts
@@ -20,6 +20,11 @@ export interface OkDialogRequest {
message: string;
}
+export interface CancelDialogRequest {
+ title: string;
+ message: string;
+}
+
export interface YesNoDialogRequest {
title: string;
message: string;
@@ -53,6 +58,65 @@ export interface EditControllerServiceDialogRequest {
controllerService: ControllerServiceEntity;
}
+export interface ProvenanceEventSummary {
+ id: string;
+ eventId: number;
+ eventTime: string;
+ eventType: string;
+ flowFileUuid: string;
+ fileSize: string;
+ fileSizeBytes: number;
+ clusterNodeId?: string;
+ clusterNodeAddress?: string;
+ groupId: string;
+ componentId: string;
+ componentType: string;
+ componentName: string;
+}
+
+export interface Attribute {
+ name: string;
+ value: string;
+ previousValue: string;
+}
+
+export interface ProvenanceEvent extends ProvenanceEventSummary {
+ eventDuration: string;
+ lineageDuration: number;
+ clusterNodeId: string;
+ clusterNodeAddress: string;
+ sourceSystemFlowFileId: string;
+ alternateIdentifierUri: string;
+ attributes: Attribute[];
+ parentUuids: string[];
+ childUuids: string[];
+ transitUri: string;
+ relationship: string;
+ details: string;
+ contentEqual: boolean;
+ inputContentAvailable: boolean;
+ inputContentClaimSection: string;
+ inputContentClaimContainer: string;
+ inputContentClaimIdentifier: string;
+ inputContentClaimOffset: number;
+ inputContentClaimFileSize: string;
+ inputContentClaimFileSizeBytes: number;
+ outputContentAvailable: boolean;
+ outputContentClaimSection: string;
+ outputContentClaimContainer: string;
+ outputContentClaimIdentifier: string;
+ outputContentClaimOffset: string;
+ outputContentClaimFileSize: string;
+ outputContentClaimFileSizeBytes: number;
+ replayAvailable: boolean;
+ replayExplanation: string;
+ sourceConnectionIdentifier: string;
+}
+
+export interface ProvenanceEventDialogRequest {
+ event: ProvenanceEvent;
+}
+
export interface TextTipInput {
text: string;
}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.html
new file mode 100644
index 0000000000..da55132180
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.html
@@ -0,0 +1,24 @@
+<!--
+ ~ 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>{{ request.title }}</h2>
+<mat-dialog-content>
+ <div class="text-sm">{{ request.message }}</div>
+</mat-dialog-content>
+<mat-dialog-actions align="end">
+ <button type="button" (click)="cancelClicked()" mat-raised-button mat-dialog-close color="primary">Cancel</button>
+</mat-dialog-actions>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.scss
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.scss
index 3789433cd3..2944f98194 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.scss
@@ -14,22 +14,3 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
-
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
-}
-
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.spec.ts
new file mode 100644
index 0000000000..b3ae96a090
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.spec.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CancelDialog } from './cancel-dialog.component';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+
+describe('CancelDialog', () => {
+ let component: CancelDialog;
+ let fixture: ComponentFixture<CancelDialog>;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [CancelDialog],
+ providers: [
+ {
+ provide: MAT_DIALOG_DATA,
+ useValue: {
+ title: 'Title',
+ message: 'Message'
+ }
+ }
+ ]
+ });
+ fixture = TestBed.createComponent(CancelDialog);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.ts
similarity index 52%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.ts
index 3789433cd3..16a4583049 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/cancel-dialog/cancel-dialog.component.ts
@@ -15,21 +15,24 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+import { Component, EventEmitter, Inject, Output } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatButtonModule } from '@angular/material/button';
+import { CancelDialogRequest } from '../../../state/shared';
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+@Component({
+ selector: 'cancel-dialog',
+ standalone: true,
+ imports: [MatDialogModule, MatButtonModule],
+ templateUrl: './cancel-dialog.component.html',
+ styleUrls: ['./cancel-dialog.component.scss']
+})
+export class CancelDialog {
+ @Output() cancel: EventEmitter<void> = new EventEmitter<void>();
+
+ constructor(@Inject(MAT_DIALOG_DATA) public request: CancelDialogRequest) {}
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
+ cancelClicked(): void {
+ this.cancel.next();
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts
index 75b2ca8b1f..a2208ca3ba 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/property-table/property-table.component.ts
@@ -260,8 +260,8 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
type: property.descriptor.required
? 'required'
: property.descriptor.dynamic
- ? 'userDefined'
- : 'optional'
+ ? 'userDefined'
+ : 'optional'
};
this.populateServiceLink(item);
@@ -307,8 +307,8 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
type: property.descriptor.required
? 'required'
: property.descriptor.dynamic
- ? 'userDefined'
- : 'optional'
+ ? 'userDefined'
+ : 'optional'
};
this.populateServiceLink(item);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.html b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.html
new file mode 100644
index 0000000000..de1cdd511c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.html
@@ -0,0 +1,447 @@
+<!--
+ ~ 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>Provenance Event</h2>
+<div class="provenance-event">
+ <mat-dialog-content>
+ <mat-tab-group>
+ <mat-tab label="Details">
+ <div class="tab-content py-4">
+ <div class="absolute inset-0 flex gap-x-4">
+ <div class="w-full flex flex-col gap-y-3">
+ <div class="flex flex-col">
+ <div>Time</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.eventTime }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div>Event Duration</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatDuration;
+ context: { $implicit: request.event.eventDuration }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div>Lineage Duration</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatDuration;
+ context: { $implicit: request.event.lineageDuration }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div>Type</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.eventType }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div>FlowFile UUID</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.flowFileUuid }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div>File Size</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: {
+ $implicit: request.event.fileSize,
+ title: request.event.fileSizeBytes + ' bytes'
+ }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div>Component Id</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.componentId }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div>Component Name</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.componentName }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div>Component Type</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.componentType }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div>Details</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.details }
+ "></ng-container>
+ </div>
+ <ng-container *ngIf="request.event.eventType === 'RECEIVE'">
+ <div class="flex flex-col">
+ <div>Source FlowFile Id</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.sourceSystemFlowFileId }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div>Transit Uri</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.transitUri }
+ "></ng-container>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="request.event.eventType === 'SEND'">
+ <div class="flex flex-col">
+ <div>Transit Uri</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.transitUri }
+ "></ng-container>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="request.event.eventType === 'ADDINFO'">
+ <div class="flex flex-col">
+ <div>Alternate Identifier Uri</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.alternateIdentifierUri }
+ "></ng-container>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="request.event.eventType === 'ROUTE'">
+ <div class="flex flex-col">
+ <div>Relationship</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.relationship }
+ "></ng-container>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="request.event.eventType === 'FETCH'">
+ <div class="flex flex-col" *ngIf="request.event.eventType === 'FETCH'">
+ <div>Transit Uri</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.transitUri }
+ "></ng-container>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="request.event.clusterNodeId">
+ <div class="flex flex-col">
+ <div>Node Address</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: request.event.clusterNodeAddress }
+ "></ng-container>
+ </div>
+ </ng-container>
+ <ng-template #formatDuration let-duration>
+ <ng-container *ngIf="duration != null; else noDuration">
+ <div class="value">{{ formatDurationValue(duration) }}</div>
+ </ng-container>
+ <ng-template #noDuration>
+ <div class="unset">No value set</div>
+ </ng-template>
+ </ng-template>
+ </div>
+ <div class="w-full flex flex-col gap-y-3">
+ <div class="flex flex-col">
+ <div class="event-header">
+ Parent FlowFiles ({{ request.event.parentUuids.length }})
+ </div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatUuids;
+ context: { $implicit: request.event.parentUuids, emptyMessage: 'No parents' }
+ "></ng-container>
+ </div>
+ <div class="flex flex-col">
+ <div class="event-header">Child FlowFiles ({{ request.event.childUuids.length }})</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatUuids;
+ context: { $implicit: request.event.childUuids, emptyMessage: 'No children' }
+ "></ng-container>
+ </div>
+ <ng-template #formatUuids let-uuids let-emptyMessage="emptyMessage">
+ <ng-container *ngIf="uuids.length > 0; else noUuids">
+ <div class="value">
+ <div *ngFor="let uuid of uuids">{{ uuid }}</div>
+ </div>
+ </ng-container>
+ <ng-template #noUuids>
+ <div class="unset">{{ emptyMessage }}</div>
+ </ng-template>
+ </ng-template>
+ </div>
+ </div>
+ </div>
+ </mat-tab>
+ <mat-tab label="Attributes">
+ <div class="tab-content py-4">
+ <div class="absolute inset-0 flex flex-col gap-y-4">
+ <div class="flex justify-between">
+ <div class="event-header">Attribute Values</div>
+ <div class="flex items-center gap-x-1">
+ <mat-checkbox [(ngModel)]="onlyShowModifiedAttributes"></mat-checkbox>
+ <div>Show modified attributes only</div>
+ </div>
+ </div>
+ <div class="flex flex-col">
+ <div *ngFor="let attribute of request.event.attributes">
+ <div *ngIf="shouldShowAttribute(attribute)" class="mb-4 flex flex-col">
+ <div>{{ attribute.name }}</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: attribute.value }
+ "></ng-container>
+ <ng-container *ngIf="attributeValueChanged(attribute)">
+ <ng-container
+ *ngTemplateOutlet="
+ formatValue;
+ context: { $implicit: attribute.previousValue }
+ "></ng-container>
+ </ng-container>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </mat-tab>
+ <mat-tab label="Content">
+ <div class="tab-content py-4">
+ <div class="absolute inset-0 flex flex-col gap-y-4">
+ <div class="flex gap-x-4">
+ <div class="w-full">
+ <div class="event-header">Input Claim</div>
+ <div class="flex flex-col gap-y-3">
+ <div>
+ <div>Container</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: { $implicit: request.event.inputContentClaimContainer }
+ "></ng-container>
+ </div>
+ <div>
+ <div>Section</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: { $implicit: request.event.inputContentClaimSection }
+ "></ng-container>
+ </div>
+ <div>
+ <div>Identifier</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: { $implicit: request.event.inputContentClaimIdentifier }
+ "></ng-container>
+ </div>
+ <div>
+ <div>Offset</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: { $implicit: request.event.inputContentClaimOffset }
+ "></ng-container>
+ </div>
+ <div>
+ <div>Size</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: {
+ $implicit: request.event.inputContentClaimFileSize,
+ title: request.event.inputContentClaimFileSizeBytes + ' bytes'
+ }
+ "></ng-container>
+ </div>
+ <div *ngIf="request.event.inputContentAvailable" class="flex">
+ <button
+ color="accent"
+ mat-raised-button
+ (click)="downloadContentClicked('input')">
+ <i class="fa fa-download"></i>
+ Download
+ </button>
+ <button
+ *ngIf="contentViewerAvailable"
+ class="ml-3"
+ color="accent"
+ mat-raised-button
+ (click)="viewContentClicked('input')">
+ <i class="fa fa-eye"></i>
+ View
+ </button>
+ </div>
+ </div>
+ </div>
+ <div class="w-full">
+ <div class="event-header">Output Claim</div>
+ <div class="flex flex-col gap-y-3">
+ <div>
+ <div>Container</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: { $implicit: request.event.outputContentClaimContainer }
+ "></ng-container>
+ </div>
+ <div>
+ <div>Section</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: { $implicit: request.event.outputContentClaimSection }
+ "></ng-container>
+ </div>
+ <div>
+ <div>Identifier</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: { $implicit: request.event.outputContentClaimIdentifier }
+ "></ng-container>
+ </div>
+ <div>
+ <div>Offset</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: { $implicit: request.event.outputContentClaimOffset }
+ "></ng-container>
+ </div>
+ <div>
+ <div>Size</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: {
+ $implicit: request.event.outputContentClaimFileSize,
+ title: request.event.outputContentClaimFileSizeBytes + ' bytes'
+ }
+ "></ng-container>
+ </div>
+ <div *ngIf="request.event.outputContentAvailable" class="flex">
+ <button
+ color="accent"
+ mat-raised-button
+ (click)="downloadContentClicked('output')">
+ <i class="fa fa-download"></i>
+ Download
+ </button>
+ <button
+ *ngIf="contentViewerAvailable"
+ class="ml-3"
+ color="accent"
+ mat-raised-button
+ (click)="viewContentClicked('output')">
+ <i class="fa fa-eye"></i>
+ View
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="flex flex-col">
+ <div class="event-header">Replay</div>
+ <div
+ *ngIf="request.event.replayAvailable; else replayNotAvailable"
+ class="flex flex-col gap-y-3">
+ <div class="flex flex-col">
+ <div>Connection Id</div>
+ <ng-container
+ *ngTemplateOutlet="
+ formatContentValue;
+ context: { $implicit: request.event.sourceConnectionIdentifier }
+ "></ng-container>
+ </div>
+ <div>
+ <button color="accent" mat-raised-button mat-dialog-close (click)="replayClicked()">
+ <i class="fa fa-repeat"></i>
+ Replay
+ </button>
+ </div>
+ </div>
+ <ng-template #replayNotAvailable>
+ <div>{{ request.event.replayExplanation }}</div>
+ </ng-template>
+ </div>
+ </div>
+ </div>
+ </mat-tab>
+ </mat-tab-group>
+ <ng-template #formatValue let-value let-title="title">
+ <ng-container *ngIf="value != null; else nullValue">
+ <ng-container *ngIf="value === ''; else nonEmptyValue">
+ <div class="unset">Empty string set</div>
+ </ng-container>
+ <ng-template #nonEmptyValue>
+ <div class="value" *ngIf="title == null; else valueWithTitle">{{ value }}</div>
+ <ng-template #valueWithTitle>
+ <div class="value" [title]="title">{{ value }}</div>
+ </ng-template>
+ </ng-template>
+ </ng-container>
+ <ng-template #nullValue>
+ <div class="unset">No value set</div>
+ </ng-template>
+ </ng-template>
+ <ng-template #formatContentValue let-value let-title="title">
+ <ng-container *ngIf="value != null; else nullValue">
+ <div class="value" *ngIf="title == null; else valueWithTitle">{{ value }}</div>
+ <ng-template #valueWithTitle>
+ <div class="value" [title]="title">{{ value }}</div>
+ </ng-template>
+ </ng-container>
+ <ng-template #nullValue>
+ <div class="unset">No value previously set</div>
+ </ng-template>
+ </ng-template>
+ </mat-dialog-content>
+ <mat-dialog-actions align="end">
+ <button color="primary" mat-raised-button mat-dialog-close>Ok</button>
+ </mat-dialog-actions>
+</div>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.scss
similarity index 53%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
copy to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.scss
index 3789433cd3..688f7ebbb1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/state/index.ts
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.scss
@@ -15,21 +15,35 @@
* limitations under the License.
*/
-import { routerReducer, RouterReducerState } from '@ngrx/router-store';
-import { ActionReducerMap } from '@ngrx/store';
-import { UserState, userFeatureKey } from './user';
-import { userReducer } from './user/user.reducer';
-import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
-import { extensionTypesReducer } from './extension-types/extension-types.reducer';
+@use '@angular/material' as mat;
-export interface NiFiState {
- router: RouterReducerState;
- [userFeatureKey]: UserState;
- [extensionTypesFeatureKey]: ExtensionTypesState;
-}
+.provenance-event {
+ @include mat.button-density(-1);
+
+ .mdc-dialog__content {
+ padding: 0 16px;
+ font-size: 14px;
+
+ .tab-content {
+ position: relative;
+ height: 475px;
+ overflow-y: auto;
-export const rootReducers: ActionReducerMap<NiFiState> = {
- router: routerReducer,
- [userFeatureKey]: userReducer,
- [extensionTypesFeatureKey]: extensionTypesReducer
-};
+ .event-header {
+ color: #728e9b;
+ font-size: 15px;
+ font-family: 'Roboto Slab';
+ font-style: normal;
+ font-weight: bold;
+ }
+ }
+ }
+
+ .mat-mdc-form-field {
+ width: 100%;
+ }
+
+ .mdc-text-field__input::-webkit-calendar-picker-indicator {
+ display: block !important;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.spec.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.spec.ts
new file mode 100644
index 0000000000..fb7f240034
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.spec.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ProvenanceEventDialog } from './provenance-event-dialog.component';
+import { MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+describe('ProvenanceEventDialog', () => {
+ let component: ProvenanceEventDialog;
+ let fixture: ComponentFixture<ProvenanceEventDialog>;
+
+ const data: any = {
+ event: {
+ id: '67231',
+ eventId: 67231,
+ eventTime: '12/06/2023 13:24:14.934 EST',
+ lineageDuration: 80729691,
+ eventType: 'DROP',
+ flowFileUuid: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc',
+ fileSize: '0 bytes',
+ fileSizeBytes: 0,
+ groupId: '36d207b9-018c-1000-6f5a-1b02d4517a78',
+ componentId: '36d293df-018c-1000-b438-9e0cd664f5aa',
+ componentType: 'Connection',
+ componentName: 'success',
+ attributes: [
+ {
+ name: 'filename',
+ value: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc',
+ previousValue: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc'
+ },
+ {
+ name: 'path',
+ value: './',
+ previousValue: './'
+ },
+ {
+ name: 'uuid',
+ value: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc',
+ previousValue: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc'
+ }
+ ],
+ parentUuids: [],
+ childUuids: [],
+ details: 'FlowFile Queue emptied by admin',
+ contentEqual: false,
+ inputContentAvailable: false,
+ outputContentAvailable: false,
+ outputContentClaimFileSize: '0 bytes',
+ outputContentClaimFileSizeBytes: 0,
+ replayAvailable: true,
+ sourceConnectionIdentifier: '36d293df-018c-1000-b438-9e0cd664f5aa'
+ }
+ };
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [ProvenanceEventDialog, BrowserAnimationsModule],
+ providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
+ });
+ fixture = TestBed.createComponent(ProvenanceEventDialog);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.ts b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.ts
new file mode 100644
index 0000000000..7d8f2176f9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/app/ui/common/provenance-event-dialog/provenance-event-dialog.component.ts
@@ -0,0 +1,98 @@
+/*
+ * 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, EventEmitter, Inject, Input, Output } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatInputModule } from '@angular/material/input';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+import { MatButtonModule } from '@angular/material/button';
+import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { NiFiCommon } from '../../../service/nifi-common.service';
+import { MatTabsModule } from '@angular/material/tabs';
+import { Attribute, ProvenanceEvent, ProvenanceEventDialogRequest } from '../../../state/shared';
+
+@Component({
+ selector: 'provenance-event-dialog',
+ standalone: true,
+ templateUrl: './provenance-event-dialog.component.html',
+ imports: [
+ ReactiveFormsModule,
+ MatDialogModule,
+ MatInputModule,
+ MatCheckboxModule,
+ MatButtonModule,
+ NgIf,
+ AsyncPipe,
+ NgForOf,
+ MatDatepickerModule,
+ MatTabsModule,
+ NgTemplateOutlet,
+ FormsModule
+ ],
+ styleUrls: ['./provenance-event-dialog.component.scss']
+})
+export class ProvenanceEventDialog {
+ @Input() contentViewerAvailable!: boolean;
+
+ @Output() downloadContent: EventEmitter<string> = new EventEmitter<string>();
+ @Output() viewContent: EventEmitter<string> = new EventEmitter<string>();
+ @Output() replay: EventEmitter<void> = new EventEmitter<void>();
+
+ onlyShowModifiedAttributes: boolean = false;
+
+ constructor(
+ @Inject(MAT_DIALOG_DATA) public request: ProvenanceEventDialogRequest,
+ private nifiCommon: NiFiCommon
+ ) {}
+
+ formatDurationValue(duration: number): string {
+ if (duration === 0) {
+ return '< 1 sec';
+ }
+
+ return this.nifiCommon.formatDuration(duration);
+ }
+
+ attributeValueChanged(attribute: Attribute): boolean {
+ return attribute.value != attribute.previousValue;
+ }
+
+ shouldShowAttribute(attribute: Attribute): boolean {
+ // if the attribute value has changed, show it
+ if (this.attributeValueChanged(attribute)) {
+ return true;
+ }
+
+ // attribute value hasn't changed, only show when
+ // the user does not want to only see modified attributes
+ return !this.onlyShowModifiedAttributes;
+ }
+
+ downloadContentClicked(direction: string): void {
+ this.downloadContent.next(direction);
+ }
+
+ viewContentClicked(direction: string): void {
+ this.viewContent.next(direction);
+ }
+
+ replayClicked(): void {
+ this.replay.next();
+ }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss
index edce9be5e7..7632849f0b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-frontend/src/main/nifi/src/styles.scss
@@ -20,11 +20,11 @@
@use '@angular/material' as mat;
// Plus imports for other components in your app.
-@import '~roboto-fontface/css/roboto/roboto-fontface.css';
-@import 'assets/fonts/flowfont/flowfont.css';
-@import '~font-awesome/css/font-awesome.min.css';
-@import '~codemirror/lib/codemirror.css';
-@import '~codemirror/addon/hint/show-hint.css';
+@use 'roboto-fontface/css/roboto/roboto-fontface.css';
+@use 'assets/fonts/flowfont/flowfont.css';
+@use 'font-awesome/css/font-awesome.min.css';
+@use 'codemirror/lib/codemirror.css';
+@use 'codemirror/addon/hint/show-hint.css';
$fontPrimary: 'Roboto', sans-serif;
$fontSecondary: 'Robot Slab', sans-serif;