Skip to content

Commit 32d7018

Browse files
committed
NIFI-14431 - Added "Details & Attributes" side panel to content viewer.
1 parent c73cc71 commit 32d7018

18 files changed

Lines changed: 1358 additions & 37 deletions

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/feature/content-viewer.component.html

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,16 @@
5555
</div>
5656
</form>
5757
</div>
58-
<div class="p-2 flex-1 flex border overflow-y-auto">
59-
@if (viewerSelected) {
60-
<router-outlet></router-outlet>
61-
} @else {
62-
<div class="unset">No data reference specified</div>
63-
}
58+
<div class="p-2 flex-1 flex overflow-y-auto">
59+
<div class="p-2 flex-1 flex border overflow-y-auto">
60+
@if (viewerSelected) {
61+
<router-outlet></router-outlet>
62+
} @else {
63+
<div class="unset">No data reference specified</div>
64+
}
65+
</div>
66+
<div *ngIf="data.flowFile || data.provEvent">
67+
<content-details [data]="data" (downloadContent)="downloadContent($event)"></content-details>
68+
</div>
6469
</div>
6570
</div>

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/feature/content-viewer.component.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ import { ContentViewerComponent } from './content-viewer.component';
2121
import { provideMockStore } from '@ngrx/store/testing';
2222
import { contentViewersFeatureKey } from '../state';
2323
import { viewerOptionsFeatureKey } from '../state/viewer-options';
24-
import { initialState } from '../state/viewer-options/viewer-options.reducer';
24+
import { initialState as viewerOptionsInitialState } from '../state/viewer-options/viewer-options.reducer';
2525
import { MatSelectModule } from '@angular/material/select';
2626
import { ReactiveFormsModule } from '@angular/forms';
2727
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
2828
import { MatIconModule } from '@angular/material/icon';
2929
import { NifiTooltipDirective } from '@nifi/shared';
30+
import { contentFeatureKey } from '../state/content';
31+
import { initialState as contentInitialState } from '../state/content/content.reducer';
3032

3133
describe('ContentViewerComponent', () => {
3234
let component: ContentViewerComponent;
@@ -40,7 +42,8 @@ describe('ContentViewerComponent', () => {
4042
provideMockStore({
4143
initialState: {
4244
[contentViewersFeatureKey]: {
43-
[viewerOptionsFeatureKey]: initialState
45+
[viewerOptionsFeatureKey]: viewerOptionsInitialState,
46+
[contentFeatureKey]: contentInitialState
4447
}
4548
}
4649
})

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/feature/content-viewer.component.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,22 @@ import { ContentViewer, HEX_VIEWER_URL, SupportedMimeTypes } from '../state/view
2525
import { isDefinedAndNotNull, NiFiCommon, SelectGroup, SelectOption, selectQueryParams, TextTip } from '@nifi/shared';
2626
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2727
import { concatLatestFrom } from '@ngrx/operators';
28-
import { navigateToBundledContentViewer, resetContent, setRef } from '../state/content/content.actions';
28+
import {
29+
downloadContentWithEvent,
30+
downloadContentWithFlowFile,
31+
loadSideBarData,
32+
navigateToBundledContentViewer,
33+
resetContent,
34+
setRef
35+
} from '../state/content/content.actions';
2936
import { MatSelectChange } from '@angular/material/select';
3037
import { loadAbout } from '../../../state/about/about.actions';
3138
import { selectAbout } from '../../../state/about/about.selectors';
3239
import { filter, map, switchMap, take } from 'rxjs';
3340
import { navigateToExternalViewer } from '../state/external-viewer/external-viewer.actions';
3441
import { snackBarError } from '../../../state/error/error.actions';
42+
import { selectSideBarData } from '../state/content/content.selectors';
43+
import { SideBarData } from '../state/content';
3544

3645
interface SupportedContentViewer {
3746
supportedMimeTypes: SupportedMimeTypes;
@@ -49,6 +58,7 @@ export class ContentViewerComponent implements OnInit, OnDestroy {
4958
viewerForm: FormGroup;
5059
viewAsOptions: SelectGroup[] = [];
5160
panelWidth: string | null = 'auto';
61+
data!: SideBarData;
5262

5363
private supportedMimeTypeId = 0;
5464
private supportedContentViewerLookup: Map<number, SupportedContentViewer> = new Map<
@@ -61,6 +71,9 @@ export class ContentViewerComponent implements OnInit, OnDestroy {
6171
viewerSelected: boolean = false;
6272
mimeType: string | undefined;
6373
private clientId: string | undefined;
74+
private uri: string | undefined;
75+
private eventId: number | undefined;
76+
private clusterNodeId: string | undefined;
6477

6578
private queryParamsLoaded = false;
6679
private viewerOptionsLoaded = false;
@@ -72,6 +85,10 @@ export class ContentViewerComponent implements OnInit, OnDestroy {
7285
) {
7386
this.viewerForm = this.formBuilder.group({ viewAs: null });
7487

88+
this.store.select(selectSideBarData).subscribe((data) => {
89+
this.data = data;
90+
});
91+
7592
this.store
7693
.select(selectViewerOptions)
7794
.pipe(
@@ -199,7 +216,9 @@ export class ContentViewerComponent implements OnInit, OnDestroy {
199216
const ref = queryParams['ref'];
200217
this.mimeType = queryParams['mimeType'];
201218
this.clientId = queryParams['clientId'];
202-
219+
this.uri = queryParams['uri'];
220+
this.clusterNodeId = queryParams['clusterNodeId'];
221+
this.eventId = queryParams['eventId'];
203222
if (ref) {
204223
this.store.dispatch(
205224
setRef({
@@ -219,6 +238,7 @@ export class ContentViewerComponent implements OnInit, OnDestroy {
219238
ngOnInit(): void {
220239
this.store.dispatch(loadContentViewerOptions());
221240
this.store.dispatch(loadAbout());
241+
this.store.dispatch(loadSideBarData());
222242
}
223243

224244
private handleDefaultSelection(): void {
@@ -303,6 +323,27 @@ export class ContentViewerComponent implements OnInit, OnDestroy {
303323
}
304324
}
305325

326+
downloadContent(direction?: string): void {
327+
if (this.eventId && direction) {
328+
this.store.dispatch(
329+
downloadContentWithEvent({
330+
eventId: this.eventId,
331+
direction: direction,
332+
clusterNodeId: this.clusterNodeId
333+
})
334+
);
335+
} else if (this.uri) {
336+
this.store.dispatch(
337+
downloadContentWithFlowFile({
338+
request: {
339+
uri: this.uri,
340+
clusterNodeId: this.clusterNodeId
341+
}
342+
})
343+
);
344+
}
345+
}
346+
306347
ngOnDestroy(): void {
307348
this.store.dispatch(resetContent());
308349
this.store.dispatch(resetContentViewerOptions());

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/feature/content-viewer.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { ImageViewer } from '../ui/image-viewer/image-viewer.component';
3030
import { ContentEffects } from '../state/content/content.effects';
3131
import { ExternalViewerEffects } from '../state/external-viewer/external-viewer.effects';
3232
import { NifiTooltipDirective } from '@nifi/shared';
33+
import { ContentDetails } from '../ui/content-details/content-details.component';
3334

3435
@NgModule({
3536
declarations: [ContentViewerComponent],
@@ -45,7 +46,8 @@ import { NifiTooltipDirective } from '@nifi/shared';
4546
HexViewer,
4647
ImageViewer,
4748
NgOptimizedImage,
48-
NifiTooltipDirective
49+
NifiTooltipDirective,
50+
ContentDetails
4951
]
5052
})
5153
export class ContentViewerModule {}

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/state/content/content.actions.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717

1818
import { createAction, props } from '@ngrx/store';
19+
import { loadSideBarDataRequestSuccess } from './index';
20+
import { DownloadFlowFileContentRequest } from '../../../queue/state/queue-listing';
1921

2022
export const setRef = createAction('[Content] Set Ref', props<{ ref: string }>());
2123

@@ -25,3 +27,20 @@ export const navigateToBundledContentViewer = createAction(
2527
);
2628

2729
export const resetContent = createAction('[Content] Reset Content');
30+
31+
export const loadSideBarData = createAction('[Content] Load SideBar Data');
32+
33+
export const loadSideBarDataSuccess = createAction(
34+
'[Content] Load SideBar Data Success',
35+
props<{ response: loadSideBarDataRequestSuccess }>()
36+
);
37+
38+
export const downloadContentWithFlowFile = createAction(
39+
'[Content] Download Content With FlowFile',
40+
props<{ request: DownloadFlowFileContentRequest }>()
41+
);
42+
43+
export const downloadContentWithEvent = createAction(
44+
'[Content] Download Content With Event',
45+
props<{ eventId: number; direction: string; clusterNodeId?: string }>()
46+
);

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/state/content/content.effects.ts

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,23 @@
1818
import { Injectable } from '@angular/core';
1919
import { Actions, createEffect, ofType } from '@ngrx/effects';
2020
import * as ContentActions from './content.actions';
21-
import { map, tap } from 'rxjs';
21+
import { catchError, from, map, of, switchMap, tap, withLatestFrom } from 'rxjs';
2222
import { NavigationExtras, Router } from '@angular/router';
23-
import { NiFiCommon } from '@nifi/shared';
23+
import { NiFiCommon, selectQueryParams } from '@nifi/shared';
24+
import { select, Store } from '@ngrx/store';
25+
import { NiFiState } from '../../../../state';
26+
import { QueueService } from '../../../queue/service/queue.service';
27+
import { ProvenanceService } from '../../../provenance/service/provenance.service';
2428

2529
@Injectable()
2630
export class ContentEffects {
2731
constructor(
2832
private actions$: Actions,
2933
private router: Router,
30-
private nifiCommon: NiFiCommon
34+
private nifiCommon: NiFiCommon,
35+
private queueService: QueueService,
36+
private provenanceService: ProvenanceService,
37+
private store: Store<NiFiState>
3138
) {}
3239

3340
navigateToBundledContentViewer$ = createEffect(
@@ -46,4 +53,99 @@ export class ContentEffects {
4653
),
4754
{ dispatch: false }
4855
);
56+
57+
loadSideBarData$ = createEffect(() =>
58+
this.actions$.pipe(
59+
ofType(ContentActions.loadSideBarData),
60+
withLatestFrom(this.store.pipe(select(selectQueryParams))),
61+
switchMap(([, params]) => {
62+
if (params['eventId']) {
63+
return from(
64+
this.provenanceService.getProvenanceEvent(params['eventId'], params['clusterNodeId'])
65+
).pipe(
66+
map((response) => {
67+
return ContentActions.loadSideBarDataSuccess({
68+
response: {
69+
data: {
70+
flowFile: undefined,
71+
provEvent: response.provenanceEvent
72+
}
73+
}
74+
});
75+
}),
76+
catchError(() =>
77+
of(
78+
ContentActions.loadSideBarDataSuccess({
79+
response: {
80+
data: {
81+
flowFile: undefined,
82+
provEvent: undefined
83+
}
84+
}
85+
})
86+
)
87+
)
88+
);
89+
} else {
90+
return from(
91+
this.queueService.getFlowFile({
92+
filename: '',
93+
lineageDuration: 0,
94+
penalized: false,
95+
penaltyExpiresIn: 0,
96+
queuedDuration: 0,
97+
size: 0,
98+
uuid: '',
99+
uri: params['uri'],
100+
clusterNodeId: params['clusterNodeId']
101+
})
102+
).pipe(
103+
map((response) => {
104+
return ContentActions.loadSideBarDataSuccess({
105+
response: {
106+
data: {
107+
flowFile: response.flowFile,
108+
provEvent: undefined
109+
}
110+
}
111+
});
112+
}),
113+
catchError(() =>
114+
of(
115+
ContentActions.loadSideBarDataSuccess({
116+
response: {
117+
data: {
118+
flowFile: undefined,
119+
provEvent: undefined
120+
}
121+
}
122+
})
123+
)
124+
)
125+
);
126+
}
127+
})
128+
)
129+
);
130+
131+
downloadContentWithFlowFile$ = createEffect(
132+
() => () =>
133+
this.actions$.pipe(
134+
ofType(ContentActions.downloadContentWithFlowFile),
135+
map((action) => action.request),
136+
tap((request) => this.queueService.downloadContent(request))
137+
),
138+
{ dispatch: false }
139+
);
140+
141+
downloadContentWithEvent$ = createEffect(
142+
() => () =>
143+
this.actions$.pipe(
144+
ofType(ContentActions.downloadContentWithEvent),
145+
map((request) => {
146+
this.provenanceService.downloadContent(request.eventId, request.direction, request.clusterNodeId);
147+
})
148+
),
149+
{ dispatch: false }
150+
);
49151
}

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/state/content/content.reducer.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,14 @@
1717

1818
import { createReducer, on } from '@ngrx/store';
1919
import { ContentState } from './index';
20-
import { resetContent, setRef } from './content.actions';
20+
import { loadSideBarDataSuccess, resetContent, setRef } from './content.actions';
2121

2222
export const initialState: ContentState = {
23-
ref: null
23+
ref: null,
24+
data: {
25+
provEvent: undefined,
26+
flowFile: undefined
27+
}
2428
};
2529

2630
export const contentReducer = createReducer(
@@ -31,5 +35,9 @@ export const contentReducer = createReducer(
3135
})),
3236
on(resetContent, () => ({
3337
...initialState
38+
})),
39+
on(loadSideBarDataSuccess, (state, { response }) => ({
40+
...state,
41+
data: response.data
3442
}))
3543
);

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/state/content/content.selectors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ export const selectContentState = createSelector(
2525
);
2626

2727
export const selectRef = createSelector(selectContentState, (state: ContentState) => state.ref);
28+
29+
export const selectSideBarData = createSelector(selectContentState, (state: ContentState) => state.data);

nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/content-viewer/state/content/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,21 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { FlowFile } from '../../../queue/state/queue-listing';
19+
import { ProvenanceEvent } from '../../../../state/shared';
20+
1821
export const contentFeatureKey = 'content';
1922

2023
export interface ContentState {
2124
ref: string | null;
25+
data: SideBarData;
26+
}
27+
28+
export interface loadSideBarDataRequestSuccess {
29+
data: SideBarData;
30+
}
31+
32+
export interface SideBarData {
33+
flowFile: FlowFile | undefined;
34+
provEvent: ProvenanceEvent | undefined;
2235
}

0 commit comments

Comments
 (0)