mirror of https://github.com/apache/nifi.git
NIFI-12807: Handle clustering in Provenance, Lineage, and Queue Listing (#8431)
* NIFI-12807: - Handling cluster node id in provenance listing, lineage graph, and queue listing. * NIFI-12807: - Addressing review feedback. This closes #8431
This commit is contained in:
parent
0a2ba317c0
commit
6c76ecadd4
|
@ -64,7 +64,8 @@
|
||||||
"buildTarget": "nifi:build:production"
|
"buildTarget": "nifi:build:production"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"buildTarget": "nifi:build:development"
|
"buildTarget": "nifi:build:development",
|
||||||
|
"servePath": "/nifi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
|
|
|
@ -9,9 +9,5 @@ const target = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'/nifi-api/*': target,
|
'/': target
|
||||||
'/nifi-docs/*': target,
|
|
||||||
'/nifi-content-viewer/*': target,
|
|
||||||
// the following entry is needed because the content viewer (and other UIs) load resources from existing nifi ui
|
|
||||||
'/nifi/*': target
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -46,6 +46,7 @@ import { ErrorEffects } from './state/error/error.effects';
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { PipesModule } from './pipes/pipes.module';
|
import { PipesModule } from './pipes/pipes.module';
|
||||||
import { DocumentationEffects } from './state/documentation/documentation.effects';
|
import { DocumentationEffects } from './state/documentation/documentation.effects';
|
||||||
|
import { ClusterSummaryEffects } from './state/cluster-summary/cluster-summary.effects';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
|
@ -73,7 +74,8 @@ import { DocumentationEffects } from './state/documentation/documentation.effect
|
||||||
ControllerServiceStateEffects,
|
ControllerServiceStateEffects,
|
||||||
SystemDiagnosticsEffects,
|
SystemDiagnosticsEffects,
|
||||||
ComponentStateEffects,
|
ComponentStateEffects,
|
||||||
DocumentationEffects
|
DocumentationEffects,
|
||||||
|
ClusterSummaryEffects
|
||||||
),
|
),
|
||||||
StoreDevtoolsModule.instrument({
|
StoreDevtoolsModule.instrument({
|
||||||
maxAge: 25,
|
maxAge: 25,
|
||||||
|
|
|
@ -68,10 +68,6 @@ export class FlowService implements PropertyDescriptorRetriever {
|
||||||
return this.httpClient.get(`${FlowService.API}/flow/status`);
|
return this.httpClient.get(`${FlowService.API}/flow/status`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getClusterSummary(): Observable<any> {
|
|
||||||
return this.httpClient.get(`${FlowService.API}/flow/cluster/summary`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getControllerBulletins(): Observable<any> {
|
getControllerBulletins(): Observable<any> {
|
||||||
return this.httpClient.get(`${FlowService.API}/flow/controller/bulletins`);
|
return this.httpClient.get(`${FlowService.API}/flow/controller/bulletins`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,6 @@ import { ImportFromRegistry } from '../../ui/canvas/items/flow/import-from-regis
|
||||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||||
import { NoRegistryClientsDialog } from '../../ui/common/no-registry-clients-dialog/no-registry-clients-dialog.component';
|
import { NoRegistryClientsDialog } from '../../ui/common/no-registry-clients-dialog/no-registry-clients-dialog.component';
|
||||||
import { EditRemoteProcessGroup } from '../../ui/canvas/items/remote-process-group/edit-remote-process-group/edit-remote-process-group.component';
|
import { EditRemoteProcessGroup } from '../../ui/canvas/items/remote-process-group/edit-remote-process-group/edit-remote-process-group.component';
|
||||||
import { ErrorHelper } from '../../../../service/error-helper.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FlowEffects {
|
export class FlowEffects {
|
||||||
|
@ -144,16 +143,14 @@ export class FlowEffects {
|
||||||
combineLatest([
|
combineLatest([
|
||||||
this.flowService.getFlow(request.id),
|
this.flowService.getFlow(request.id),
|
||||||
this.flowService.getFlowStatus(),
|
this.flowService.getFlowStatus(),
|
||||||
this.flowService.getClusterSummary(),
|
|
||||||
this.flowService.getControllerBulletins()
|
this.flowService.getControllerBulletins()
|
||||||
]).pipe(
|
]).pipe(
|
||||||
map(([flow, flowStatus, clusterSummary, controllerBulletins]) => {
|
map(([flow, flowStatus, controllerBulletins]) => {
|
||||||
return FlowActions.loadProcessGroupSuccess({
|
return FlowActions.loadProcessGroupSuccess({
|
||||||
response: {
|
response: {
|
||||||
id: request.id,
|
id: request.id,
|
||||||
flow: flow,
|
flow: flow,
|
||||||
flowStatus: flowStatus,
|
flowStatus: flowStatus,
|
||||||
clusterSummary: clusterSummary.clusterSummary,
|
|
||||||
controllerBulletins: controllerBulletins
|
controllerBulletins: controllerBulletins
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -123,13 +123,6 @@ export const initialState: FlowState = {
|
||||||
syncFailureCount: undefined
|
syncFailureCount: undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clusterSummary: {
|
|
||||||
clustered: false,
|
|
||||||
connectedToCluster: false,
|
|
||||||
connectedNodes: '',
|
|
||||||
connectedNodeCount: 0,
|
|
||||||
totalNodeCount: 0
|
|
||||||
},
|
|
||||||
refreshRpgDetails: null,
|
refreshRpgDetails: null,
|
||||||
controllerBulletins: {
|
controllerBulletins: {
|
||||||
bulletins: [],
|
bulletins: [],
|
||||||
|
@ -182,7 +175,6 @@ export const flowReducer = createReducer(
|
||||||
id: response.flow.processGroupFlow.id,
|
id: response.flow.processGroupFlow.id,
|
||||||
flow: response.flow,
|
flow: response.flow,
|
||||||
flowStatus: response.flowStatus,
|
flowStatus: response.flowStatus,
|
||||||
clusterSummary: response.clusterSummary,
|
|
||||||
controllerBulletins: response.controllerBulletins,
|
controllerBulletins: response.controllerBulletins,
|
||||||
error: null,
|
error: null,
|
||||||
status: 'success' as const
|
status: 'success' as const
|
||||||
|
|
|
@ -228,8 +228,6 @@ export const selectLastRefreshed = createSelector(
|
||||||
(state: FlowState) => state.flow.processGroupFlow.lastRefreshed
|
(state: FlowState) => state.flow.processGroupFlow.lastRefreshed
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectClusterSummary = createSelector(selectFlowState, (state: FlowState) => state.clusterSummary);
|
|
||||||
|
|
||||||
export const selectControllerBulletins = createSelector(
|
export const selectControllerBulletins = createSelector(
|
||||||
selectFlowState,
|
selectFlowState,
|
||||||
(state: FlowState) => state.controllerBulletins.bulletins // TODO - include others?
|
(state: FlowState) => state.controllerBulletins.bulletins // TODO - include others?
|
||||||
|
|
|
@ -62,7 +62,6 @@ export interface LoadProcessGroupResponse {
|
||||||
id: string;
|
id: string;
|
||||||
flow: ProcessGroupFlowEntity;
|
flow: ProcessGroupFlowEntity;
|
||||||
flowStatus: ControllerStatusEntity;
|
flowStatus: ControllerStatusEntity;
|
||||||
clusterSummary: ClusterSummary;
|
|
||||||
controllerBulletins: ControllerBulletinsEntity;
|
controllerBulletins: ControllerBulletinsEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,14 +492,6 @@ export interface ControllerStatusEntity {
|
||||||
controllerStatus: ControllerStatus;
|
controllerStatus: ControllerStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClusterSummary {
|
|
||||||
clustered: boolean;
|
|
||||||
connectedToCluster: boolean;
|
|
||||||
connectedNodes?: string;
|
|
||||||
connectedNodeCount: number;
|
|
||||||
totalNodeCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ControllerBulletinsEntity {
|
export interface ControllerBulletinsEntity {
|
||||||
bulletins: BulletinEntity[];
|
bulletins: BulletinEntity[];
|
||||||
controllerServiceBulletins: BulletinEntity[];
|
controllerServiceBulletins: BulletinEntity[];
|
||||||
|
@ -514,7 +505,6 @@ export interface FlowState {
|
||||||
flow: ProcessGroupFlowEntity;
|
flow: ProcessGroupFlowEntity;
|
||||||
flowStatus: ControllerStatusEntity;
|
flowStatus: ControllerStatusEntity;
|
||||||
refreshRpgDetails: RefreshRemoteProcessGroupPollingDetailsRequest | null;
|
refreshRpgDetails: RefreshRemoteProcessGroupPollingDetailsRequest | null;
|
||||||
clusterSummary: ClusterSummary;
|
|
||||||
controllerBulletins: ControllerBulletinsEntity;
|
controllerBulletins: ControllerBulletinsEntity;
|
||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
transitionRequired: boolean;
|
transitionRequired: boolean;
|
||||||
|
|
|
@ -67,6 +67,11 @@ import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow
|
||||||
import { concatLatestFrom } from '@ngrx/effects';
|
import { concatLatestFrom } from '@ngrx/effects';
|
||||||
import { selectUrl } from '../../../../state/router/router.selectors';
|
import { selectUrl } from '../../../../state/router/router.selectors';
|
||||||
import { Storage } from '../../../../service/storage.service';
|
import { Storage } from '../../../../service/storage.service';
|
||||||
|
import {
|
||||||
|
loadClusterSummary,
|
||||||
|
startClusterSummaryPolling,
|
||||||
|
stopClusterSummaryPolling
|
||||||
|
} from '../../../../state/cluster-summary/cluster-summary.actions';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'fd-canvas',
|
selector: 'fd-canvas',
|
||||||
|
@ -285,7 +290,9 @@ export class Canvas implements OnInit, OnDestroy {
|
||||||
this.canvasView.init(this.viewContainerRef, this.svg, this.canvas);
|
this.canvasView.init(this.viewContainerRef, this.svg, this.canvas);
|
||||||
|
|
||||||
this.store.dispatch(loadFlowConfiguration());
|
this.store.dispatch(loadFlowConfiguration());
|
||||||
|
this.store.dispatch(loadClusterSummary());
|
||||||
this.store.dispatch(startProcessGroupPolling());
|
this.store.dispatch(startProcessGroupPolling());
|
||||||
|
this.store.dispatch(startClusterSummaryPolling());
|
||||||
}
|
}
|
||||||
|
|
||||||
private createSvg(): void {
|
private createSvg(): void {
|
||||||
|
@ -595,5 +602,6 @@ export class Canvas implements OnInit, OnDestroy {
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.store.dispatch(resetFlowState());
|
this.store.dispatch(resetFlowState());
|
||||||
this.store.dispatch(stopProcessGroupPolling());
|
this.store.dispatch(stopProcessGroupPolling());
|
||||||
|
this.store.dispatch(stopClusterSummaryPolling());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,10 @@
|
||||||
color: $primary-palette-500;
|
color: $primary-palette-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: $warn-palette-400;
|
||||||
|
}
|
||||||
|
|
||||||
.status-value {
|
.status-value {
|
||||||
color: $warn-palette-A400;
|
color: $warn-palette-A400;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<div class="h-8 flow-status">
|
<div class="h-8 flow-status">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div class="flex flex-1 justify-around pr-20">
|
<div class="flex flex-1 justify-around pr-20">
|
||||||
@if (clusterSummary.clustered) {
|
@if (clusterSummary?.clustered) {
|
||||||
<div class="flex items-center gap-x-2" title="Connected nodes / Total number of nodes in the cluster">
|
<div class="flex items-center gap-x-2" title="Connected nodes / Total number of nodes in the cluster">
|
||||||
<div class="fa fa-cubes" [class]="getClusterStyle()"></div>
|
<div class="fa fa-cubes" [class]="getClusterStyle()"></div>
|
||||||
<div class="text">{{ formatClusterMessage() }}</div>
|
<div class="text">{{ formatClusterMessage() }}</div>
|
||||||
|
|
|
@ -16,13 +16,14 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { ClusterSummary, ControllerStatus } from '../../../../state/flow';
|
import { ControllerStatus } from '../../../../state/flow';
|
||||||
import { initialState } from '../../../../state/flow/flow.reducer';
|
import { initialState } from '../../../../state/flow/flow.reducer';
|
||||||
import { BulletinsTip } from '../../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
|
import { BulletinsTip } from '../../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
|
||||||
import { BulletinEntity, BulletinsTipInput } from '../../../../../../state/shared';
|
import { BulletinEntity, BulletinsTipInput } from '../../../../../../state/shared';
|
||||||
|
|
||||||
import { Search } from '../search/search.component';
|
import { Search } from '../search/search.component';
|
||||||
import { NifiTooltipDirective } from '../../../../../../ui/common/tooltips/nifi-tooltip.directive';
|
import { NifiTooltipDirective } from '../../../../../../ui/common/tooltips/nifi-tooltip.directive';
|
||||||
|
import { ClusterSummary } from '../../../../../../state/cluster-summary';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'flow-status',
|
selector: 'flow-status',
|
||||||
|
@ -34,7 +35,7 @@ import { NifiTooltipDirective } from '../../../../../../ui/common/tooltips/nifi-
|
||||||
export class FlowStatus {
|
export class FlowStatus {
|
||||||
@Input() controllerStatus: ControllerStatus = initialState.flowStatus.controllerStatus;
|
@Input() controllerStatus: ControllerStatus = initialState.flowStatus.controllerStatus;
|
||||||
@Input() lastRefreshed: string = initialState.flow.processGroupFlow.lastRefreshed;
|
@Input() lastRefreshed: string = initialState.flow.processGroupFlow.lastRefreshed;
|
||||||
@Input() clusterSummary: ClusterSummary = initialState.clusterSummary;
|
@Input() clusterSummary: ClusterSummary | null = null;
|
||||||
@Input() bulletins: BulletinEntity[] = initialState.controllerBulletins.bulletins;
|
@Input() bulletins: BulletinEntity[] = initialState.controllerBulletins.bulletins;
|
||||||
@Input() currentProcessGroupId: string = initialState.id;
|
@Input() currentProcessGroupId: string = initialState.id;
|
||||||
@Input() loadingStatus = false;
|
@Input() loadingStatus = false;
|
||||||
|
@ -46,7 +47,7 @@ export class FlowStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
formatClusterMessage(): string {
|
formatClusterMessage(): string {
|
||||||
if (this.clusterSummary.connectedToCluster && this.clusterSummary.connectedNodes) {
|
if (this.clusterSummary?.connectedToCluster && this.clusterSummary.connectedNodes) {
|
||||||
return this.clusterSummary.connectedNodes;
|
return this.clusterSummary.connectedNodes;
|
||||||
} else {
|
} else {
|
||||||
return 'Disconnected';
|
return 'Disconnected';
|
||||||
|
@ -55,8 +56,8 @@ export class FlowStatus {
|
||||||
|
|
||||||
getClusterStyle(): string {
|
getClusterStyle(): string {
|
||||||
if (
|
if (
|
||||||
!this.clusterSummary.connectedToCluster ||
|
this.clusterSummary?.connectedToCluster === false ||
|
||||||
this.clusterSummary.connectedNodeCount != this.clusterSummary.totalNodeCount
|
this.clusterSummary?.connectedNodeCount != this.clusterSummary?.totalNodeCount
|
||||||
) {
|
) {
|
||||||
return 'warning';
|
return 'warning';
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,16 +24,14 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
import { NewCanvasItem } from './new-canvas-item/new-canvas-item.component';
|
import { NewCanvasItem } from './new-canvas-item/new-canvas-item.component';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatDividerModule } from '@angular/material/divider';
|
import { MatDividerModule } from '@angular/material/divider';
|
||||||
import {
|
import { selectControllerBulletins, selectControllerStatus } from '../../../state/flow/flow.selectors';
|
||||||
selectClusterSummary,
|
import { ControllerStatus } from '../../../state/flow';
|
||||||
selectControllerBulletins,
|
|
||||||
selectControllerStatus
|
|
||||||
} from '../../../state/flow/flow.selectors';
|
|
||||||
import { ClusterSummary, ControllerStatus } from '../../../state/flow';
|
|
||||||
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { ClusterSummary } from '../../../../../state/cluster-summary';
|
||||||
|
import { selectClusterSummary } from '../../../../../state/cluster-summary/cluster-summary.selectors';
|
||||||
|
|
||||||
describe('HeaderComponent', () => {
|
describe('HeaderComponent', () => {
|
||||||
let component: HeaderComponent;
|
let component: HeaderComponent;
|
||||||
|
|
|
@ -21,7 +21,6 @@ import { Store } from '@ngrx/store';
|
||||||
import { CanvasState } from '../../../state';
|
import { CanvasState } from '../../../state';
|
||||||
import {
|
import {
|
||||||
selectCanvasPermissions,
|
selectCanvasPermissions,
|
||||||
selectClusterSummary,
|
|
||||||
selectControllerBulletins,
|
selectControllerBulletins,
|
||||||
selectControllerStatus,
|
selectControllerStatus,
|
||||||
selectCurrentProcessGroupId,
|
selectCurrentProcessGroupId,
|
||||||
|
@ -36,6 +35,7 @@ import { MatDividerModule } from '@angular/material/divider';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { FlowStatus } from './flow-status/flow-status.component';
|
import { FlowStatus } from './flow-status/flow-status.component';
|
||||||
import { Navigation } from '../../../../../ui/common/navigation/navigation.component';
|
import { Navigation } from '../../../../../ui/common/navigation/navigation.component';
|
||||||
|
import { selectClusterSummary } from '../../../../../state/cluster-summary/cluster-summary.selectors';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'fd-header',
|
selector: 'fd-header',
|
||||||
|
|
|
@ -22,7 +22,6 @@ import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||||
import { ParameterContextEntity } from '../../../state/parameter-context-listing';
|
import { ParameterContextEntity } from '../../../state/parameter-context-listing';
|
||||||
import { FlowConfiguration } from '../../../../../state/flow-configuration';
|
import { FlowConfiguration } from '../../../../../state/flow-configuration';
|
||||||
import { CurrentUser } from '../../../../../state/current-user';
|
import { CurrentUser } from '../../../../../state/current-user';
|
||||||
import { ParameterProviderConfigurationEntity } from '../../../../../state/shared';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'parameter-context-table',
|
selector: 'parameter-context-table',
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { ProvenanceRequest } from '../state/provenance-event-listing';
|
import { ProvenanceRequest } from '../state/provenance-event-listing';
|
||||||
import { LineageRequest } from '../state/lineage';
|
import { LineageRequest } from '../state/lineage';
|
||||||
|
|
||||||
|
@ -36,28 +36,44 @@ export class ProvenanceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
getProvenanceQuery(id: string, clusterNodeId?: string): Observable<any> {
|
getProvenanceQuery(id: string, clusterNodeId?: string): Observable<any> {
|
||||||
// TODO - cluster node id
|
let params = new HttpParams().set('summarize', true).set('incrementalResults', false);
|
||||||
return this.httpClient.get(`${ProvenanceService.API}/provenance/${encodeURIComponent(id)}`);
|
if (clusterNodeId) {
|
||||||
|
params = params.set('clusterNodeId', clusterNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient.get(`${ProvenanceService.API}/provenance/${encodeURIComponent(id)}`, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteProvenanceQuery(id: string, clusterNodeId?: string): Observable<any> {
|
deleteProvenanceQuery(id: string, clusterNodeId?: string): Observable<any> {
|
||||||
// TODO - cluster node id
|
let params = new HttpParams();
|
||||||
return this.httpClient.delete(`${ProvenanceService.API}/provenance/${encodeURIComponent(id)}`);
|
if (clusterNodeId) {
|
||||||
|
params = params.set('clusterNodeId', clusterNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient.delete(`${ProvenanceService.API}/provenance/${encodeURIComponent(id)}`, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
getProvenanceEvent(id: string): Observable<any> {
|
getProvenanceEvent(eventId: number, clusterNodeId?: string): Observable<any> {
|
||||||
// TODO - cluster node id
|
let params = new HttpParams();
|
||||||
return this.httpClient.get(`${ProvenanceService.API}/provenance-events/${encodeURIComponent(id)}`);
|
if (clusterNodeId) {
|
||||||
|
params = params.set('clusterNodeId', clusterNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient.get(`${ProvenanceService.API}/provenance-events/${encodeURIComponent(eventId)}`, {
|
||||||
|
params
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadContent(id: string, direction: string): void {
|
downloadContent(eventId: number, direction: string, clusterNodeId?: string): void {
|
||||||
let dataUri = `${ProvenanceService.API}/provenance-events/${encodeURIComponent(
|
let dataUri = `${ProvenanceService.API}/provenance-events/${encodeURIComponent(
|
||||||
id
|
eventId
|
||||||
)}/content/${encodeURIComponent(direction)}`;
|
)}/content/${encodeURIComponent(direction)}`;
|
||||||
|
|
||||||
const queryParameters: any = {};
|
const queryParameters: any = {};
|
||||||
|
|
||||||
// TODO - cluster node id in query parameters
|
if (clusterNodeId) {
|
||||||
|
queryParameters['clusterNodeId'] = clusterNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(queryParameters).length > 0) {
|
if (Object.keys(queryParameters).length > 0) {
|
||||||
const query: string = new URLSearchParams(queryParameters).toString();
|
const query: string = new URLSearchParams(queryParameters).toString();
|
||||||
|
@ -67,13 +83,23 @@ export class ProvenanceService {
|
||||||
window.open(dataUri);
|
window.open(dataUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewContent(nifiUrl: string, contentViewerUrl: string, id: string, direction: string): void {
|
viewContent(
|
||||||
|
nifiUrl: string,
|
||||||
|
contentViewerUrl: string,
|
||||||
|
eventId: number,
|
||||||
|
direction: string,
|
||||||
|
clusterNodeId?: string
|
||||||
|
): void {
|
||||||
// build the uri to the data
|
// build the uri to the data
|
||||||
let dataUri = `${nifiUrl}provenance-events/${encodeURIComponent(id)}/content/${encodeURIComponent(direction)}`;
|
let dataUri = `${nifiUrl}provenance-events/${encodeURIComponent(eventId)}/content/${encodeURIComponent(
|
||||||
|
direction
|
||||||
|
)}`;
|
||||||
|
|
||||||
const dataUriParameters: any = {};
|
const dataUriParameters: any = {};
|
||||||
|
|
||||||
// TODO - cluster node id in data uri parameters
|
if (clusterNodeId) {
|
||||||
|
dataUriParameters['clusterNodeId'] = clusterNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
// include parameters if necessary
|
// include parameters if necessary
|
||||||
if (Object.keys(dataUriParameters).length > 0) {
|
if (Object.keys(dataUriParameters).length > 0) {
|
||||||
|
@ -100,12 +126,14 @@ export class ProvenanceService {
|
||||||
window.open(`${contentViewer}${contentViewerQuery}`);
|
window.open(`${contentViewer}${contentViewerQuery}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
replay(eventId: string): Observable<any> {
|
replay(eventId: number, clusterNodeId?: string): Observable<any> {
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
eventId
|
eventId
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO - add cluster node id in payload
|
if (clusterNodeId) {
|
||||||
|
payload['clusterNodeId'] = clusterNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
return this.httpClient.post(`${ProvenanceService.API}/provenance-events/replays`, payload);
|
return this.httpClient.post(`${ProvenanceService.API}/provenance-events/replays`, payload);
|
||||||
}
|
}
|
||||||
|
@ -115,12 +143,22 @@ export class ProvenanceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
getLineageQuery(id: string, clusterNodeId?: string): Observable<any> {
|
getLineageQuery(id: string, clusterNodeId?: string): Observable<any> {
|
||||||
// TODO - cluster node id
|
let params = new HttpParams();
|
||||||
return this.httpClient.get(`${ProvenanceService.API}/provenance/lineage/${encodeURIComponent(id)}`);
|
if (clusterNodeId) {
|
||||||
|
params = params.set('clusterNodeId', clusterNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient.get(`${ProvenanceService.API}/provenance/lineage/${encodeURIComponent(id)}`, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteLineageQuery(id: string, clusterNodeId?: string): Observable<any> {
|
deleteLineageQuery(id: string, clusterNodeId?: string): Observable<any> {
|
||||||
// TODO - cluster node id
|
let params = new HttpParams();
|
||||||
return this.httpClient.delete(`${ProvenanceService.API}/provenance/lineage/${encodeURIComponent(id)}`);
|
if (clusterNodeId) {
|
||||||
|
params = params.set('clusterNodeId', clusterNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient.delete(`${ProvenanceService.API}/provenance/lineage/${encodeURIComponent(id)}`, {
|
||||||
|
params
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,7 @@ import { Store } from '@ngrx/store';
|
||||||
import { NiFiState } from '../../../../state';
|
import { NiFiState } from '../../../../state';
|
||||||
import { ProvenanceService } from '../../service/provenance.service';
|
import { ProvenanceService } from '../../service/provenance.service';
|
||||||
import { Lineage } from './index';
|
import { Lineage } from './index';
|
||||||
import { selectClusterNodeId } from '../provenance-event-listing/provenance-event-listing.selectors';
|
import { selectActiveLineageId, selectClusterNodeIdFromActiveLineage } from './lineage.selectors';
|
||||||
import { selectActiveLineageId } from './lineage.selectors';
|
|
||||||
import * as ErrorActions from '../../../../state/error/error.actions';
|
import * as ErrorActions from '../../../../state/error/error.actions';
|
||||||
import { ErrorHelper } from '../../../../service/error-helper.service';
|
import { ErrorHelper } from '../../../../service/error-helper.service';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
@ -105,7 +104,7 @@ export class LineageEffects {
|
||||||
ofType(LineageActions.pollLineageQuery),
|
ofType(LineageActions.pollLineageQuery),
|
||||||
concatLatestFrom(() => [
|
concatLatestFrom(() => [
|
||||||
this.store.select(selectActiveLineageId).pipe(isDefinedAndNotNull()),
|
this.store.select(selectActiveLineageId).pipe(isDefinedAndNotNull()),
|
||||||
this.store.select(selectClusterNodeId)
|
this.store.select(selectClusterNodeIdFromActiveLineage)
|
||||||
]),
|
]),
|
||||||
switchMap(([, id, clusterNodeId]) =>
|
switchMap(([, id, clusterNodeId]) =>
|
||||||
from(this.provenanceService.getLineageQuery(id, clusterNodeId)).pipe(
|
from(this.provenanceService.getLineageQuery(id, clusterNodeId)).pipe(
|
||||||
|
@ -153,7 +152,10 @@ export class LineageEffects {
|
||||||
deleteLineageQuery$ = createEffect(() =>
|
deleteLineageQuery$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(LineageActions.deleteLineageQuery),
|
ofType(LineageActions.deleteLineageQuery),
|
||||||
concatLatestFrom(() => [this.store.select(selectActiveLineageId), this.store.select(selectClusterNodeId)]),
|
concatLatestFrom(() => [
|
||||||
|
this.store.select(selectActiveLineageId),
|
||||||
|
this.store.select(selectClusterNodeIdFromActiveLineage)
|
||||||
|
]),
|
||||||
tap(([, id, clusterNodeId]) => {
|
tap(([, id, clusterNodeId]) => {
|
||||||
if (id) {
|
if (id) {
|
||||||
this.provenanceService.deleteLineageQuery(id, clusterNodeId).subscribe();
|
this.provenanceService.deleteLineageQuery(id, clusterNodeId).subscribe();
|
||||||
|
|
|
@ -32,3 +32,8 @@ export const selectCompletedLineage = createSelector(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectActiveLineageId = createSelector(selectActiveLineage, (state: Lineage | null) => state?.id);
|
export const selectActiveLineageId = createSelector(selectActiveLineage, (state: Lineage | null) => state?.id);
|
||||||
|
|
||||||
|
export const selectClusterNodeIdFromActiveLineage = createSelector(
|
||||||
|
selectActiveLineage,
|
||||||
|
(state: Lineage | null) => state?.request.clusterNodeId
|
||||||
|
);
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ProvenanceEventSummary } from '../../../../state/shared';
|
import { ProvenanceEventSummary } from '../../../../state/shared';
|
||||||
|
import { NodeSearchResult } from '../../../../state/cluster-summary';
|
||||||
|
|
||||||
export const provenanceEventListingFeatureKey = 'provenanceEventListing';
|
export const provenanceEventListingFeatureKey = 'provenanceEventListing';
|
||||||
|
|
||||||
|
@ -33,14 +34,15 @@ export interface ProvenanceQueryResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProvenanceEventRequest {
|
export interface ProvenanceEventRequest {
|
||||||
id: string;
|
eventId: number;
|
||||||
clusterNodeId?: string;
|
clusterNodeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GoToProvenanceEventSourceRequest {
|
export interface GoToProvenanceEventSourceRequest {
|
||||||
eventId?: string;
|
eventId?: number;
|
||||||
componentId?: string;
|
componentId?: string;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
|
clusterNodeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchableField {
|
export interface SearchableField {
|
||||||
|
@ -54,8 +56,13 @@ export interface ProvenanceOptions {
|
||||||
searchableFields: SearchableField[];
|
searchableFields: SearchableField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenSearchRequest {
|
||||||
|
clusterNodes: NodeSearchResult[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProvenanceSearchDialogRequest {
|
export interface ProvenanceSearchDialogRequest {
|
||||||
timeOffset: number;
|
timeOffset: number;
|
||||||
|
clusterNodes: NodeSearchResult[];
|
||||||
options: ProvenanceOptions;
|
options: ProvenanceOptions;
|
||||||
currentRequest: ProvenanceRequest;
|
currentRequest: ProvenanceRequest;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
import { createAction, props } from '@ngrx/store';
|
import { createAction, props } from '@ngrx/store';
|
||||||
import {
|
import {
|
||||||
GoToProvenanceEventSourceRequest,
|
GoToProvenanceEventSourceRequest,
|
||||||
|
OpenSearchRequest,
|
||||||
ProvenanceEventRequest,
|
ProvenanceEventRequest,
|
||||||
ProvenanceOptionsResponse,
|
ProvenanceOptionsResponse,
|
||||||
ProvenanceQueryResponse,
|
ProvenanceQueryResponse,
|
||||||
|
@ -78,7 +79,14 @@ export const goToProvenanceEventSource = createAction(
|
||||||
props<{ request: GoToProvenanceEventSourceRequest }>()
|
props<{ request: GoToProvenanceEventSourceRequest }>()
|
||||||
);
|
);
|
||||||
|
|
||||||
export const openSearchDialog = createAction('[Provenance Event Listing] Open Search Dialog');
|
export const loadClusterNodesAndOpenSearchDialog = createAction(
|
||||||
|
'[Provenance Event Listing] Load Cluster Nodes And Open Search Dialog'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const openSearchDialog = createAction(
|
||||||
|
'[Provenance Event Listing] Open Search Dialog',
|
||||||
|
props<{ request: OpenSearchRequest }>()
|
||||||
|
);
|
||||||
|
|
||||||
export const saveProvenanceRequest = createAction(
|
export const saveProvenanceRequest = createAction(
|
||||||
'[Provenance Event Listing] Save Provenance Request',
|
'[Provenance Event Listing] Save Provenance Request',
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { Router } from '@angular/router';
|
||||||
import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
|
import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
|
||||||
import { ProvenanceService } from '../../service/provenance.service';
|
import { ProvenanceService } from '../../service/provenance.service';
|
||||||
import {
|
import {
|
||||||
selectClusterNodeId,
|
selectClusterNodeIdFromActiveProvenance,
|
||||||
selectActiveProvenanceId,
|
selectActiveProvenanceId,
|
||||||
selectProvenanceOptions,
|
selectProvenanceOptions,
|
||||||
selectProvenanceRequest,
|
selectProvenanceRequest,
|
||||||
|
@ -41,6 +41,8 @@ import * as ErrorActions from '../../../../state/error/error.actions';
|
||||||
import { ErrorHelper } from '../../../../service/error-helper.service';
|
import { ErrorHelper } from '../../../../service/error-helper.service';
|
||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { isDefinedAndNotNull } from '../../../../state/shared';
|
import { isDefinedAndNotNull } from '../../../../state/shared';
|
||||||
|
import { selectClusterSummary } from '../../../../state/cluster-summary/cluster-summary.selectors';
|
||||||
|
import { ClusterService } from '../../../../service/cluster.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ProvenanceEventListingEffects {
|
export class ProvenanceEventListingEffects {
|
||||||
|
@ -49,6 +51,7 @@ export class ProvenanceEventListingEffects {
|
||||||
private store: Store<NiFiState>,
|
private store: Store<NiFiState>,
|
||||||
private provenanceService: ProvenanceService,
|
private provenanceService: ProvenanceService,
|
||||||
private errorHelper: ErrorHelper,
|
private errorHelper: ErrorHelper,
|
||||||
|
private clusterService: ClusterService,
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
private router: Router
|
private router: Router
|
||||||
) {}
|
) {}
|
||||||
|
@ -166,7 +169,7 @@ export class ProvenanceEventListingEffects {
|
||||||
ofType(ProvenanceEventListingActions.pollProvenanceQuery),
|
ofType(ProvenanceEventListingActions.pollProvenanceQuery),
|
||||||
concatLatestFrom(() => [
|
concatLatestFrom(() => [
|
||||||
this.store.select(selectActiveProvenanceId).pipe(isDefinedAndNotNull()),
|
this.store.select(selectActiveProvenanceId).pipe(isDefinedAndNotNull()),
|
||||||
this.store.select(selectClusterNodeId)
|
this.store.select(selectClusterNodeIdFromActiveProvenance)
|
||||||
]),
|
]),
|
||||||
switchMap(([, id, clusterNodeId]) =>
|
switchMap(([, id, clusterNodeId]) =>
|
||||||
from(this.provenanceService.getProvenanceQuery(id, clusterNodeId)).pipe(
|
from(this.provenanceService.getProvenanceQuery(id, clusterNodeId)).pipe(
|
||||||
|
@ -216,7 +219,7 @@ export class ProvenanceEventListingEffects {
|
||||||
ofType(ProvenanceEventListingActions.deleteProvenanceQuery),
|
ofType(ProvenanceEventListingActions.deleteProvenanceQuery),
|
||||||
concatLatestFrom(() => [
|
concatLatestFrom(() => [
|
||||||
this.store.select(selectActiveProvenanceId),
|
this.store.select(selectActiveProvenanceId),
|
||||||
this.store.select(selectClusterNodeId)
|
this.store.select(selectClusterNodeIdFromActiveProvenance)
|
||||||
]),
|
]),
|
||||||
tap(([, id, clusterNodeId]) => {
|
tap(([, id, clusterNodeId]) => {
|
||||||
this.dialog.closeAll();
|
this.dialog.closeAll();
|
||||||
|
@ -229,20 +232,53 @@ export class ProvenanceEventListingEffects {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
loadClusterNodesAndOpenSearchDialog$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(ProvenanceEventListingActions.loadClusterNodesAndOpenSearchDialog),
|
||||||
|
concatLatestFrom(() => this.store.select(selectClusterSummary).pipe(isDefinedAndNotNull())),
|
||||||
|
switchMap(([, clusterSummary]) => {
|
||||||
|
if (clusterSummary.connectedToCluster) {
|
||||||
|
return from(this.clusterService.searchCluster()).pipe(
|
||||||
|
map((response) =>
|
||||||
|
ProvenanceEventListingActions.openSearchDialog({
|
||||||
|
request: {
|
||||||
|
clusterNodes: response.nodeResults
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
catchError((errorResponse: HttpErrorResponse) =>
|
||||||
|
of(ErrorActions.snackBarError({ error: errorResponse.error }))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return of(
|
||||||
|
ProvenanceEventListingActions.openSearchDialog({
|
||||||
|
request: {
|
||||||
|
clusterNodes: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
openSearchDialog$ = createEffect(
|
openSearchDialog$ = createEffect(
|
||||||
() =>
|
() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(ProvenanceEventListingActions.openSearchDialog),
|
ofType(ProvenanceEventListingActions.openSearchDialog),
|
||||||
|
map((action) => action.request),
|
||||||
concatLatestFrom(() => [
|
concatLatestFrom(() => [
|
||||||
this.store.select(selectTimeOffset),
|
this.store.select(selectTimeOffset),
|
||||||
this.store.select(selectProvenanceOptions),
|
this.store.select(selectProvenanceOptions),
|
||||||
this.store.select(selectProvenanceRequest),
|
this.store.select(selectProvenanceRequest),
|
||||||
this.store.select(selectAbout).pipe(isDefinedAndNotNull())
|
this.store.select(selectAbout).pipe(isDefinedAndNotNull())
|
||||||
]),
|
]),
|
||||||
tap(([, timeOffset, options, currentRequest, about]) => {
|
tap(([request, timeOffset, options, currentRequest, about]) => {
|
||||||
const dialogReference = this.dialog.open(ProvenanceSearchDialog, {
|
const dialogReference = this.dialog.open(ProvenanceSearchDialog, {
|
||||||
data: {
|
data: {
|
||||||
timeOffset,
|
timeOffset,
|
||||||
|
clusterNodes: request.clusterNodes,
|
||||||
options,
|
options,
|
||||||
currentRequest
|
currentRequest
|
||||||
},
|
},
|
||||||
|
@ -283,7 +319,7 @@ export class ProvenanceEventListingEffects {
|
||||||
map((action) => action.request),
|
map((action) => action.request),
|
||||||
concatLatestFrom(() => this.store.select(selectAbout)),
|
concatLatestFrom(() => this.store.select(selectAbout)),
|
||||||
tap(([request, about]) => {
|
tap(([request, about]) => {
|
||||||
this.provenanceService.getProvenanceEvent(request.id).subscribe({
|
this.provenanceService.getProvenanceEvent(request.eventId, request.clusterNodeId).subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
const dialogReference = this.dialog.open(ProvenanceEventDialog, {
|
const dialogReference = this.dialog.open(ProvenanceEventDialog, {
|
||||||
data: {
|
data: {
|
||||||
|
@ -298,7 +334,11 @@ export class ProvenanceEventListingEffects {
|
||||||
dialogReference.componentInstance.downloadContent
|
dialogReference.componentInstance.downloadContent
|
||||||
.pipe(takeUntil(dialogReference.afterClosed()))
|
.pipe(takeUntil(dialogReference.afterClosed()))
|
||||||
.subscribe((direction: string) => {
|
.subscribe((direction: string) => {
|
||||||
this.provenanceService.downloadContent(request.id, direction);
|
this.provenanceService.downloadContent(
|
||||||
|
request.eventId,
|
||||||
|
direction,
|
||||||
|
request.clusterNodeId
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (about) {
|
if (about) {
|
||||||
|
@ -308,8 +348,9 @@ export class ProvenanceEventListingEffects {
|
||||||
this.provenanceService.viewContent(
|
this.provenanceService.viewContent(
|
||||||
about.uri,
|
about.uri,
|
||||||
about.contentViewerUrl,
|
about.contentViewerUrl,
|
||||||
request.id,
|
request.eventId,
|
||||||
direction
|
direction,
|
||||||
|
request.clusterNodeId
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -319,7 +360,7 @@ export class ProvenanceEventListingEffects {
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
dialogReference.close();
|
dialogReference.close();
|
||||||
|
|
||||||
this.provenanceService.replay(request.id).subscribe({
|
this.provenanceService.replay(request.eventId, request.clusterNodeId).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.store.dispatch(
|
this.store.dispatch(
|
||||||
ProvenanceEventListingActions.showOkDialog({
|
ProvenanceEventListingActions.showOkDialog({
|
||||||
|
@ -356,7 +397,7 @@ export class ProvenanceEventListingEffects {
|
||||||
map((action) => action.request),
|
map((action) => action.request),
|
||||||
tap((request) => {
|
tap((request) => {
|
||||||
if (request.eventId) {
|
if (request.eventId) {
|
||||||
this.provenanceService.getProvenanceEvent(request.eventId).subscribe({
|
this.provenanceService.getProvenanceEvent(request.eventId, request.clusterNodeId).subscribe({
|
||||||
next: (response) => {
|
next: (response) => {
|
||||||
const event: any = response.provenanceEvent;
|
const event: any = response.provenanceEvent;
|
||||||
this.router.navigate(this.getEventComponentLink(event.groupId, event.componentId));
|
this.router.navigate(this.getEventComponentLink(event.groupId, event.componentId));
|
||||||
|
|
|
@ -22,7 +22,6 @@ import {
|
||||||
provenanceEventListingFeatureKey,
|
provenanceEventListingFeatureKey,
|
||||||
ProvenanceEventListingState,
|
ProvenanceEventListingState,
|
||||||
ProvenanceQueryParams,
|
ProvenanceQueryParams,
|
||||||
ProvenanceRequest,
|
|
||||||
ProvenanceResults
|
ProvenanceResults
|
||||||
} from './index';
|
} from './index';
|
||||||
import { selectCurrentRoute } from '../../../../state/router/router.selectors';
|
import { selectCurrentRoute } from '../../../../state/router/router.selectors';
|
||||||
|
@ -78,9 +77,9 @@ export const selectCompletedProvenance = createSelector(
|
||||||
|
|
||||||
export const selectActiveProvenanceId = createSelector(selectActiveProvenance, (state: Provenance | null) => state?.id);
|
export const selectActiveProvenanceId = createSelector(selectActiveProvenance, (state: Provenance | null) => state?.id);
|
||||||
|
|
||||||
export const selectClusterNodeId = createSelector(
|
export const selectClusterNodeIdFromActiveProvenance = createSelector(
|
||||||
selectProvenanceRequest,
|
selectActiveProvenance,
|
||||||
(state: ProvenanceRequest | null) => state?.clusterNodeId
|
(state: Provenance | null) => state?.request.clusterNodeId
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectProvenanceResults = createSelector(
|
export const selectProvenanceResults = createSelector(
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
[loading]="status === 'loading'"
|
[loading]="status === 'loading'"
|
||||||
[loadedTimestamp]="(loadedTimestamp$ | async)!"
|
[loadedTimestamp]="(loadedTimestamp$ | async)!"
|
||||||
[events]="provenance.results.provenanceEvents"
|
[events]="provenance.results.provenanceEvents"
|
||||||
|
[clusterSummary]="(clusterSummary$ | async)!"
|
||||||
[oldestEventAvailable]="provenance.results.oldestEvent"
|
[oldestEventAvailable]="provenance.results.oldestEvent"
|
||||||
[timeOffset]="provenance.results.timeOffset"
|
[timeOffset]="provenance.results.timeOffset"
|
||||||
[resultsMessage]="getResultsMessage(provenance)"
|
[resultsMessage]="getResultsMessage(provenance)"
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnDestroy } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import {
|
import {
|
||||||
GoToProvenanceEventSourceRequest,
|
GoToProvenanceEventSourceRequest,
|
||||||
|
@ -37,8 +37,8 @@ import { filter, map, take, tap } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
clearProvenanceRequest,
|
clearProvenanceRequest,
|
||||||
goToProvenanceEventSource,
|
goToProvenanceEventSource,
|
||||||
|
loadClusterNodesAndOpenSearchDialog,
|
||||||
openProvenanceEventDialog,
|
openProvenanceEventDialog,
|
||||||
openSearchDialog,
|
|
||||||
resetProvenanceState,
|
resetProvenanceState,
|
||||||
resubmitProvenanceQuery,
|
resubmitProvenanceQuery,
|
||||||
saveProvenanceRequest
|
saveProvenanceRequest
|
||||||
|
@ -48,17 +48,20 @@ import { resetLineage, submitLineageQuery } from '../../state/lineage/lineage.ac
|
||||||
import { LineageRequest } from '../../state/lineage';
|
import { LineageRequest } from '../../state/lineage';
|
||||||
import { selectCompletedLineage } from '../../state/lineage/lineage.selectors';
|
import { selectCompletedLineage } from '../../state/lineage/lineage.selectors';
|
||||||
import { clearBannerErrors } from '../../../../state/error/error.actions';
|
import { clearBannerErrors } from '../../../../state/error/error.actions';
|
||||||
|
import { selectClusterSummary } from '../../../../state/cluster-summary/cluster-summary.selectors';
|
||||||
|
import { loadClusterSummary } from '../../../../state/cluster-summary/cluster-summary.actions';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'provenance-event-listing',
|
selector: 'provenance-event-listing',
|
||||||
templateUrl: './provenance-event-listing.component.html',
|
templateUrl: './provenance-event-listing.component.html',
|
||||||
styleUrls: ['./provenance-event-listing.component.scss']
|
styleUrls: ['./provenance-event-listing.component.scss']
|
||||||
})
|
})
|
||||||
export class ProvenanceEventListing implements OnDestroy {
|
export class ProvenanceEventListing implements OnInit, OnDestroy {
|
||||||
status$ = this.store.select(selectStatus);
|
status$ = this.store.select(selectStatus);
|
||||||
loadedTimestamp$ = this.store.select(selectLoadedTimestamp);
|
loadedTimestamp$ = this.store.select(selectLoadedTimestamp);
|
||||||
provenance$ = this.store.select(selectCompletedProvenance);
|
provenance$ = this.store.select(selectCompletedProvenance);
|
||||||
lineage$ = this.store.select(selectCompletedLineage);
|
lineage$ = this.store.select(selectCompletedLineage);
|
||||||
|
clusterSummary$ = this.store.select(selectClusterSummary);
|
||||||
|
|
||||||
request!: ProvenanceRequest;
|
request!: ProvenanceRequest;
|
||||||
stateReset = false;
|
stateReset = false;
|
||||||
|
@ -137,6 +140,10 @@ export class ProvenanceEventListing implements OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.store.dispatch(loadClusterSummary());
|
||||||
|
}
|
||||||
|
|
||||||
getResultsMessage(provenance: Provenance): string {
|
getResultsMessage(provenance: Provenance): string {
|
||||||
const request: ProvenanceRequest = provenance.request;
|
const request: ProvenanceRequest = provenance.request;
|
||||||
const results: ProvenanceResults = provenance.results;
|
const results: ProvenanceResults = provenance.results;
|
||||||
|
@ -166,7 +173,7 @@ export class ProvenanceEventListing implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
openSearchCriteria(): void {
|
openSearchCriteria(): void {
|
||||||
this.store.dispatch(openSearchDialog());
|
this.store.dispatch(loadClusterNodesAndOpenSearchDialog());
|
||||||
}
|
}
|
||||||
|
|
||||||
openEventDialog(request: ProvenanceEventRequest): void {
|
openEventDialog(request: ProvenanceEventRequest): void {
|
||||||
|
|
|
@ -44,10 +44,12 @@ export class LineageComponent implements OnInit {
|
||||||
@Input() set lineage(lineage: Lineage) {
|
@Input() set lineage(lineage: Lineage) {
|
||||||
if (lineage && lineage.finished) {
|
if (lineage && lineage.finished) {
|
||||||
this.addLineage(lineage.results.nodes, lineage.results.links);
|
this.addLineage(lineage.results.nodes, lineage.results.links);
|
||||||
|
|
||||||
|
this.clusterNodeId = lineage.request.clusterNodeId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input() eventId: string | null = null;
|
@Input() eventId: number | null = null;
|
||||||
|
|
||||||
@Input() set eventTimestampThreshold(eventTimestampThreshold: number) {
|
@Input() set eventTimestampThreshold(eventTimestampThreshold: number) {
|
||||||
if (this.previousEventTimestampThreshold >= 0) {
|
if (this.previousEventTimestampThreshold >= 0) {
|
||||||
|
@ -136,9 +138,9 @@ export class LineageComponent implements OnInit {
|
||||||
action: (selection: any) => {
|
action: (selection: any) => {
|
||||||
const selectionData: any = selection.datum();
|
const selectionData: any = selection.datum();
|
||||||
|
|
||||||
// TODO cluster node id
|
|
||||||
this.openEventDialog.next({
|
this.openEventDialog.next({
|
||||||
id: selectionData.id
|
eventId: Number(selectionData.id),
|
||||||
|
clusterNodeId: this.clusterNodeId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -151,7 +153,8 @@ export class LineageComponent implements OnInit {
|
||||||
action: (selection: any) => {
|
action: (selection: any) => {
|
||||||
const selectionData: any = selection.datum();
|
const selectionData: any = selection.datum();
|
||||||
this.goToProvenanceEventSource.next({
|
this.goToProvenanceEventSource.next({
|
||||||
eventId: selectionData.id
|
eventId: Number(selectionData.id),
|
||||||
|
clusterNodeId: this.clusterNodeId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -169,11 +172,10 @@ export class LineageComponent implements OnInit {
|
||||||
action: (selection: any) => {
|
action: (selection: any) => {
|
||||||
const selectionData: any = selection.datum();
|
const selectionData: any = selection.datum();
|
||||||
|
|
||||||
// TODO - cluster node id
|
|
||||||
this.submitLineageQuery.next({
|
this.submitLineageQuery.next({
|
||||||
lineageRequestType: 'PARENTS',
|
lineageRequestType: 'PARENTS',
|
||||||
eventId: selectionData.id
|
eventId: selectionData.id,
|
||||||
// clusterNodeId: clusterNodeId
|
clusterNodeId: this.clusterNodeId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -191,11 +193,10 @@ export class LineageComponent implements OnInit {
|
||||||
action: (selection: any) => {
|
action: (selection: any) => {
|
||||||
const selectionData: any = selection.datum();
|
const selectionData: any = selection.datum();
|
||||||
|
|
||||||
// TODO - cluster node id
|
|
||||||
this.submitLineageQuery.next({
|
this.submitLineageQuery.next({
|
||||||
lineageRequestType: 'CHILDREN',
|
lineageRequestType: 'CHILDREN',
|
||||||
eventId: selectionData.id
|
eventId: selectionData.id,
|
||||||
// clusterNodeId: clusterNodeId
|
clusterNodeId: this.clusterNodeId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -227,17 +228,17 @@ export class LineageComponent implements OnInit {
|
||||||
private nodeLookup: Map<string, any> = new Map<string, any>();
|
private nodeLookup: Map<string, any> = new Map<string, any>();
|
||||||
private linkLookup: Map<string, any> = new Map<string, any>();
|
private linkLookup: Map<string, any> = new Map<string, any>();
|
||||||
private previousEventTimestampThreshold = -1;
|
private previousEventTimestampThreshold = -1;
|
||||||
|
private clusterNodeId: string | undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.allMenus = new Map<string, ContextMenuDefinition>();
|
this.allMenus = new Map<string, ContextMenuDefinition>();
|
||||||
this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);
|
this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);
|
||||||
|
|
||||||
const self: LineageComponent = this;
|
|
||||||
this.lineageContextmenu = {
|
this.lineageContextmenu = {
|
||||||
getMenu(menuId: string): ContextMenuDefinition | undefined {
|
getMenu: (menuId: string): ContextMenuDefinition | undefined => {
|
||||||
return self.allMenus.get(menuId);
|
return this.allMenus.get(menuId);
|
||||||
},
|
},
|
||||||
filterMenuItem(menuItem: ContextMenuItemDefinition): boolean {
|
filterMenuItem: (menuItem: ContextMenuItemDefinition): boolean => {
|
||||||
// include if the condition matches
|
// include if the condition matches
|
||||||
if (menuItem.condition) {
|
if (menuItem.condition) {
|
||||||
const selection: any = d3.select('circle.context');
|
const selection: any = d3.select('circle.context');
|
||||||
|
@ -247,7 +248,7 @@ export class LineageComponent implements OnInit {
|
||||||
// include if there is no condition (non conditional item, separator, sub menu, etc)
|
// include if there is no condition (non conditional item, separator, sub menu, etc)
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
menuItemClicked(menuItem: ContextMenuItemDefinition) {
|
menuItemClicked: (menuItem: ContextMenuItemDefinition): void => {
|
||||||
if (menuItem.action) {
|
if (menuItem.action) {
|
||||||
const selection: any = d3.select('circle.context');
|
const selection: any = d3.select('circle.context');
|
||||||
return menuItem.action(selection);
|
return menuItem.action(selection);
|
||||||
|
@ -794,9 +795,9 @@ export class LineageComponent implements OnInit {
|
||||||
})
|
})
|
||||||
.on('dblclick', (event: MouseEvent, d: any) => {
|
.on('dblclick', (event: MouseEvent, d: any) => {
|
||||||
// show the event details
|
// show the event details
|
||||||
// TODO - cluster node id
|
|
||||||
this.openEventDialog.next({
|
this.openEventDialog.next({
|
||||||
id: d.id
|
eventId: Number(d.id),
|
||||||
|
clusterNodeId: this.clusterNodeId
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -817,7 +818,7 @@ export class LineageComponent implements OnInit {
|
||||||
.append('circle')
|
.append('circle')
|
||||||
.attr('class', 'event-circle')
|
.attr('class', 'event-circle')
|
||||||
.classed('selected', (d: any) => {
|
.classed('selected', (d: any) => {
|
||||||
return d.id === this.eventId;
|
return d.id === String(this.eventId);
|
||||||
})
|
})
|
||||||
.attr('r', 8)
|
.attr('r', 8)
|
||||||
.attr('stroke-width', 1.0)
|
.attr('stroke-width', 1.0)
|
||||||
|
|
|
@ -130,6 +130,16 @@
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Node Column -->
|
||||||
|
@if (displayedColumns.includes('node')) {
|
||||||
|
<ng-container matColumnDef="node">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>Node</th>
|
||||||
|
<td mat-cell *matCellDef="let item" [title]="item.clusterNodeAddress">
|
||||||
|
{{ item.clusterNodeAddress }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Actions Column -->
|
<!-- Actions Column -->
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef></th>
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
|
|
|
@ -18,6 +18,11 @@
|
||||||
.provenance-event-table {
|
.provenance-event-table {
|
||||||
.listing-table {
|
.listing-table {
|
||||||
table {
|
table {
|
||||||
|
.mat-column-moreDetails {
|
||||||
|
min-width: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
.mat-column-actions {
|
.mat-column-actions {
|
||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { GoToProvenanceEventSourceRequest, ProvenanceEventRequest } from '../../
|
||||||
import { MatSliderModule } from '@angular/material/slider';
|
import { MatSliderModule } from '@angular/material/slider';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
|
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
|
||||||
|
import { ClusterSummary } from '../../../../../state/cluster-summary';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'provenance-event-table',
|
selector: 'provenance-event-table',
|
||||||
|
@ -73,8 +74,10 @@ export class ProvenanceEventTable implements AfterViewInit {
|
||||||
return this.nifiCommon.stringContains(data.componentName, filterTerm, true);
|
return this.nifiCommon.stringContains(data.componentName, filterTerm, true);
|
||||||
} else if (filterColumn === this.filterColumnOptions[1]) {
|
} else if (filterColumn === this.filterColumnOptions[1]) {
|
||||||
return this.nifiCommon.stringContains(data.componentType, filterTerm, true);
|
return this.nifiCommon.stringContains(data.componentType, filterTerm, true);
|
||||||
} else {
|
} else if (filterColumn === this.filterColumnOptions[2]) {
|
||||||
return this.nifiCommon.stringContains(data.eventType, filterTerm, true);
|
return this.nifiCommon.stringContains(data.eventType, filterTerm, true);
|
||||||
|
} else {
|
||||||
|
return this.nifiCommon.stringContains(data.clusterNodeAddress, filterTerm, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.totalCount = events.length;
|
this.totalCount = events.length;
|
||||||
|
@ -98,6 +101,30 @@ export class ProvenanceEventTable implements AfterViewInit {
|
||||||
@Input() loading!: boolean;
|
@Input() loading!: boolean;
|
||||||
@Input() loadedTimestamp!: string;
|
@Input() loadedTimestamp!: string;
|
||||||
|
|
||||||
|
@Input() set clusterSummary(clusterSummary: ClusterSummary) {
|
||||||
|
if (clusterSummary?.connectedToCluster) {
|
||||||
|
// if we're connected to the cluster add a node column if it's not already present
|
||||||
|
if (!this.displayedColumns.includes('node')) {
|
||||||
|
this.displayedColumns.splice(this.displayedColumns.length - 1, 0, 'node');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.filterColumnOptions.includes('node')) {
|
||||||
|
this.filterColumnOptions.push('node');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if we're not connected to the cluster remove the node column if it is present
|
||||||
|
const nodeIndex = this.displayedColumns.indexOf('node');
|
||||||
|
if (nodeIndex > -1) {
|
||||||
|
this.displayedColumns.splice(nodeIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterNodeIndex = this.filterColumnOptions.indexOf('node');
|
||||||
|
if (filterNodeIndex > -1) {
|
||||||
|
this.filterColumnOptions.splice(filterNodeIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Input() set lineage$(lineage$: Observable<Lineage | null>) {
|
@Input() set lineage$(lineage$: Observable<Lineage | null>) {
|
||||||
this.provenanceLineage$ = lineage$.pipe(
|
this.provenanceLineage$ = lineage$.pipe(
|
||||||
tap((lineage) => {
|
tap((lineage) => {
|
||||||
|
@ -156,7 +183,6 @@ export class ProvenanceEventTable implements AfterViewInit {
|
||||||
protected readonly ValidationErrorsTip = ValidationErrorsTip;
|
protected readonly ValidationErrorsTip = ValidationErrorsTip;
|
||||||
private destroyRef: DestroyRef = inject(DestroyRef);
|
private destroyRef: DestroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
// TODO - conditionally include the cluster column
|
|
||||||
displayedColumns: string[] = [
|
displayedColumns: string[] = [
|
||||||
'moreDetails',
|
'moreDetails',
|
||||||
'eventTime',
|
'eventTime',
|
||||||
|
@ -168,7 +194,7 @@ export class ProvenanceEventTable implements AfterViewInit {
|
||||||
'actions'
|
'actions'
|
||||||
];
|
];
|
||||||
dataSource: MatTableDataSource<ProvenanceEventSummary> = new MatTableDataSource<ProvenanceEventSummary>();
|
dataSource: MatTableDataSource<ProvenanceEventSummary> = new MatTableDataSource<ProvenanceEventSummary>();
|
||||||
selectedEventId: string | null = null;
|
selectedId: string | null = null;
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||||
|
|
||||||
|
@ -185,7 +211,7 @@ export class ProvenanceEventTable implements AfterViewInit {
|
||||||
|
|
||||||
showLineage = false;
|
showLineage = false;
|
||||||
provenanceLineage$!: Observable<Lineage | null>;
|
provenanceLineage$!: Observable<Lineage | null>;
|
||||||
eventId: string | null = null;
|
eventId: number | null = null;
|
||||||
|
|
||||||
minEventTimestamp = -1;
|
minEventTimestamp = -1;
|
||||||
maxEventTimestamp = -1;
|
maxEventTimestamp = -1;
|
||||||
|
@ -253,6 +279,11 @@ export class ProvenanceEventTable implements AfterViewInit {
|
||||||
case 'componentType':
|
case 'componentType':
|
||||||
retVal = this.nifiCommon.compareString(a.componentType, b.componentType);
|
retVal = this.nifiCommon.compareString(a.componentType, b.componentType);
|
||||||
break;
|
break;
|
||||||
|
case 'node':
|
||||||
|
if (a.clusterNodeAddress && b.clusterNodeAddress) {
|
||||||
|
retVal = this.nifiCommon.compareString(a.clusterNodeAddress, b.clusterNodeAddress);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return retVal * (isAsc ? 1 : -1);
|
return retVal * (isAsc ? 1 : -1);
|
||||||
|
@ -281,7 +312,7 @@ export class ProvenanceEventTable implements AfterViewInit {
|
||||||
|
|
||||||
viewDetailsClicked(event: ProvenanceEventSummary) {
|
viewDetailsClicked(event: ProvenanceEventSummary) {
|
||||||
this.submitProvenanceEventRequest({
|
this.submitProvenanceEventRequest({
|
||||||
id: event.id,
|
eventId: event.eventId,
|
||||||
clusterNodeId: event.clusterNodeId
|
clusterNodeId: event.clusterNodeId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -291,12 +322,12 @@ export class ProvenanceEventTable implements AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
select(event: ProvenanceEventSummary): void {
|
select(event: ProvenanceEventSummary): void {
|
||||||
this.selectedEventId = event.id;
|
this.selectedId = event.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
isSelected(event: ProvenanceEventSummary): boolean {
|
isSelected(event: ProvenanceEventSummary): boolean {
|
||||||
if (this.selectedEventId) {
|
if (this.selectedId) {
|
||||||
return event.id == this.selectedEventId;
|
return event.id == this.selectedId;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -321,7 +352,7 @@ export class ProvenanceEventTable implements AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
showLineageGraph(event: ProvenanceEventSummary): void {
|
showLineageGraph(event: ProvenanceEventSummary): void {
|
||||||
this.eventId = event.id;
|
this.eventId = event.eventId;
|
||||||
this.showLineage = true;
|
this.showLineage = true;
|
||||||
|
|
||||||
this.clearBannerErrors.next();
|
this.clearBannerErrors.next();
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
<input matInput formControlName="value" type="text" />
|
<input matInput formControlName="value" type="text" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="-mt-6 mb-4">
|
<div class="-mt-6 mb-4">
|
||||||
<mat-checkbox color="primary" formControlName="inverse" name="inverse"> Exclude </mat-checkbox>
|
<mat-checkbox color="primary" formControlName="inverse" name="inverse"> Exclude</mat-checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,16 @@
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (searchLocationOptions.length > 0) {
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Search Location</mat-label>
|
||||||
|
<mat-select formControlName="searchLocation">
|
||||||
|
@for (option of searchLocationOptions; track option) {
|
||||||
|
<mat-option [value]="option.value">{{ option.text }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
<mat-dialog-actions align="end">
|
<mat-dialog-actions align="end">
|
||||||
<button color="primary" mat-stroked-button mat-dialog-close>Cancel</button>
|
<button color="primary" mat-stroked-button mat-dialog-close>Cancel</button>
|
||||||
|
|
|
@ -21,13 +21,15 @@ import { ProvenanceSearchDialog } from './provenance-search-dialog.component';
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { MatNativeDateModule } from '@angular/material/core';
|
import { MatNativeDateModule } from '@angular/material/core';
|
||||||
|
import { ProvenanceSearchDialogRequest } from '../../../state/provenance-event-listing';
|
||||||
|
|
||||||
describe('ProvenanceSearchDialog', () => {
|
describe('ProvenanceSearchDialog', () => {
|
||||||
let component: ProvenanceSearchDialog;
|
let component: ProvenanceSearchDialog;
|
||||||
let fixture: ComponentFixture<ProvenanceSearchDialog>;
|
let fixture: ComponentFixture<ProvenanceSearchDialog>;
|
||||||
|
|
||||||
const data: any = {
|
const data: ProvenanceSearchDialogRequest = {
|
||||||
timeOffset: -18000000,
|
timeOffset: -18000000,
|
||||||
|
clusterNodes: [],
|
||||||
options: {
|
options: {
|
||||||
searchableFields: [
|
searchableFields: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -29,6 +29,11 @@ import {
|
||||||
} from '../../../state/provenance-event-listing';
|
} from '../../../state/provenance-event-listing';
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||||
|
import { SelectOption } from '../../../../../state/shared';
|
||||||
|
import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
|
||||||
|
import { MatOption } from '@angular/material/autocomplete';
|
||||||
|
import { MatSelect } from '@angular/material/select';
|
||||||
|
import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'provenance-search-dialog',
|
selector: 'provenance-search-dialog',
|
||||||
|
@ -41,12 +46,16 @@ import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
MatDatepickerModule
|
MatDatepickerModule,
|
||||||
|
MatOption,
|
||||||
|
MatSelect,
|
||||||
|
NifiTooltipDirective
|
||||||
],
|
],
|
||||||
styleUrls: ['./provenance-search-dialog.component.scss']
|
styleUrls: ['./provenance-search-dialog.component.scss']
|
||||||
})
|
})
|
||||||
export class ProvenanceSearchDialog {
|
export class ProvenanceSearchDialog {
|
||||||
@Input() timezone!: string;
|
@Input() timezone!: string;
|
||||||
|
|
||||||
@Output() submitSearchCriteria: EventEmitter<ProvenanceRequest> = new EventEmitter<ProvenanceRequest>();
|
@Output() submitSearchCriteria: EventEmitter<ProvenanceRequest> = new EventEmitter<ProvenanceRequest>();
|
||||||
|
|
||||||
public static readonly MAX_RESULTS: number = 1000;
|
public static readonly MAX_RESULTS: number = 1000;
|
||||||
|
@ -55,6 +64,7 @@ export class ProvenanceSearchDialog {
|
||||||
private static readonly TIME_REGEX = /^([0-1]\d|2[0-3]):([0-5]\d):([0-5]\d)$/;
|
private static readonly TIME_REGEX = /^([0-1]\d|2[0-3]):([0-5]\d):([0-5]\d)$/;
|
||||||
|
|
||||||
provenanceOptionsForm: FormGroup;
|
provenanceOptionsForm: FormGroup;
|
||||||
|
searchLocationOptions: SelectOption[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(MAT_DIALOG_DATA) public request: ProvenanceSearchDialogRequest,
|
@Inject(MAT_DIALOG_DATA) public request: ProvenanceSearchDialogRequest,
|
||||||
|
@ -137,6 +147,31 @@ export class ProvenanceSearchDialog {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (request.clusterNodes.length > 0) {
|
||||||
|
this.searchLocationOptions = [
|
||||||
|
{
|
||||||
|
text: 'cluster',
|
||||||
|
value: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const sortedNodes = [...this.request.clusterNodes];
|
||||||
|
sortedNodes.sort((a, b) => {
|
||||||
|
return this.nifiCommon.compareString(a.address, b.address);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.searchLocationOptions.push(
|
||||||
|
...sortedNodes.map((node) => {
|
||||||
|
return {
|
||||||
|
text: node.address,
|
||||||
|
value: node.id
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.provenanceOptionsForm.addControl('searchLocation', new FormControl(null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearTime(date: Date): void {
|
private clearTime(date: Date): void {
|
||||||
|
@ -220,6 +255,15 @@ export class ProvenanceSearchDialog {
|
||||||
});
|
});
|
||||||
provenanceRequest.searchTerms = searchTerms;
|
provenanceRequest.searchTerms = searchTerms;
|
||||||
|
|
||||||
|
if (this.searchLocationOptions.length > 0) {
|
||||||
|
const searchLocation = this.provenanceOptionsForm.get('searchLocation')?.value;
|
||||||
|
if (searchLocation) {
|
||||||
|
provenanceRequest.clusterNodeId = searchLocation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.submitSearchCriteria.next(provenanceRequest);
|
this.submitSearchCriteria.next(provenanceRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected readonly TextTip = TextTip;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,15 @@
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { NiFiCommon } from '../../../service/nifi-common.service';
|
import { NiFiCommon } from '../../../service/nifi-common.service';
|
||||||
import { FlowFileSummary, ListingRequest, SubmitQueueListingRequest } from '../state/queue-listing';
|
import {
|
||||||
|
DownloadFlowFileContentRequest,
|
||||||
|
FlowFileSummary,
|
||||||
|
ListingRequest,
|
||||||
|
SubmitQueueListingRequest,
|
||||||
|
ViewFlowFileContentRequest
|
||||||
|
} from '../state/queue-listing';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class QueueService {
|
export class QueueService {
|
||||||
|
@ -35,7 +41,12 @@ export class QueueService {
|
||||||
}
|
}
|
||||||
|
|
||||||
getFlowFile(flowfileSummary: FlowFileSummary): Observable<any> {
|
getFlowFile(flowfileSummary: FlowFileSummary): Observable<any> {
|
||||||
return this.httpClient.get(this.nifiCommon.stripProtocol(flowfileSummary.uri));
|
let params = new HttpParams();
|
||||||
|
if (flowfileSummary.clusterNodeId) {
|
||||||
|
params = params.set('clusterNodeId', flowfileSummary.clusterNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient.get(this.nifiCommon.stripProtocol(flowfileSummary.uri), { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
submitQueueListingRequest(queueListingRequest: SubmitQueueListingRequest): Observable<any> {
|
submitQueueListingRequest(queueListingRequest: SubmitQueueListingRequest): Observable<any> {
|
||||||
|
@ -53,12 +64,14 @@ export class QueueService {
|
||||||
return this.httpClient.delete(this.nifiCommon.stripProtocol(listingRequest.uri));
|
return this.httpClient.delete(this.nifiCommon.stripProtocol(listingRequest.uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadContent(flowfileSummary: FlowFileSummary): void {
|
downloadContent(request: DownloadFlowFileContentRequest): void {
|
||||||
let dataUri = `${this.nifiCommon.stripProtocol(flowfileSummary.uri)}/content`;
|
let dataUri = `${this.nifiCommon.stripProtocol(request.uri)}/content`;
|
||||||
|
|
||||||
const queryParameters: any = {};
|
const queryParameters: any = {};
|
||||||
|
|
||||||
// TODO - flowFileSummary.clusterNodeId in query parameters
|
if (request.clusterNodeId) {
|
||||||
|
queryParameters['clusterNodeId'] = request.clusterNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(queryParameters).length > 0) {
|
if (Object.keys(queryParameters).length > 0) {
|
||||||
const query: string = new URLSearchParams(queryParameters).toString();
|
const query: string = new URLSearchParams(queryParameters).toString();
|
||||||
|
@ -68,13 +81,15 @@ export class QueueService {
|
||||||
window.open(dataUri);
|
window.open(dataUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewContent(flowfileSummary: FlowFileSummary, contentViewerUrl: string): void {
|
viewContent(request: ViewFlowFileContentRequest, contentViewerUrl: string): void {
|
||||||
// build the uri to the data
|
// build the uri to the data
|
||||||
let dataUri = `${this.nifiCommon.stripProtocol(flowfileSummary.uri)}/content`;
|
let dataUri = `${this.nifiCommon.stripProtocol(request.uri)}/content`;
|
||||||
|
|
||||||
const dataUriParameters: any = {};
|
const dataUriParameters: any = {};
|
||||||
|
|
||||||
// TODO - flowFileSummary.clusterNodeId in query parameters
|
if (request.clusterNodeId) {
|
||||||
|
dataUriParameters['clusterNodeId'] = request.clusterNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
// include parameters if necessary
|
// include parameters if necessary
|
||||||
if (Object.keys(dataUriParameters).length > 0) {
|
if (Object.keys(dataUriParameters).length > 0) {
|
||||||
|
|
|
@ -89,15 +89,18 @@ export interface ViewFlowFileRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadFlowFileContentRequest {
|
export interface DownloadFlowFileContentRequest {
|
||||||
flowfileSummary: FlowFileSummary;
|
uri: string;
|
||||||
|
clusterNodeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewFlowFileContentRequest {
|
export interface ViewFlowFileContentRequest {
|
||||||
flowfileSummary: FlowFileSummary;
|
uri: string;
|
||||||
|
clusterNodeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FlowFileDialogRequest {
|
export interface FlowFileDialogRequest {
|
||||||
flowfile: FlowFile;
|
flowfile: FlowFile;
|
||||||
|
clusterNodeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueueListingState {
|
export interface QueueListingState {
|
||||||
|
|
|
@ -238,7 +238,8 @@ export class QueueListingEffects {
|
||||||
map((response) =>
|
map((response) =>
|
||||||
QueueListingActions.openFlowFileDialog({
|
QueueListingActions.openFlowFileDialog({
|
||||||
request: {
|
request: {
|
||||||
flowfile: response.flowFile
|
flowfile: response.flowFile,
|
||||||
|
clusterNodeId: request.flowfileSummary.clusterNodeId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
@ -274,7 +275,10 @@ export class QueueListingEffects {
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.store.dispatch(
|
this.store.dispatch(
|
||||||
QueueListingActions.downloadFlowFileContent({
|
QueueListingActions.downloadFlowFileContent({
|
||||||
request: { flowfileSummary: request.flowfile }
|
request: {
|
||||||
|
uri: request.flowfile.uri,
|
||||||
|
clusterNodeId: request.clusterNodeId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -285,14 +289,19 @@ export class QueueListingEffects {
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.store.dispatch(
|
this.store.dispatch(
|
||||||
QueueListingActions.viewFlowFileContent({
|
QueueListingActions.viewFlowFileContent({
|
||||||
request: { flowfileSummary: request.flowfile }
|
request: {
|
||||||
|
uri: request.flowfile.uri,
|
||||||
|
clusterNodeId: request.clusterNodeId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
{ dispatch: false }
|
{
|
||||||
|
dispatch: false
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
downloadFlowFileContent$ = createEffect(
|
downloadFlowFileContent$ = createEffect(
|
||||||
|
@ -300,7 +309,7 @@ export class QueueListingEffects {
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(QueueListingActions.downloadFlowFileContent),
|
ofType(QueueListingActions.downloadFlowFileContent),
|
||||||
map((action) => action.request),
|
map((action) => action.request),
|
||||||
tap((request) => this.queueService.downloadContent(request.flowfileSummary))
|
tap((request) => this.queueService.downloadContent(request))
|
||||||
),
|
),
|
||||||
{ dispatch: false }
|
{ dispatch: false }
|
||||||
);
|
);
|
||||||
|
@ -312,7 +321,7 @@ export class QueueListingEffects {
|
||||||
map((action) => action.request),
|
map((action) => action.request),
|
||||||
concatLatestFrom(() => this.store.select(selectAbout).pipe(isDefinedAndNotNull())),
|
concatLatestFrom(() => this.store.select(selectAbout).pipe(isDefinedAndNotNull())),
|
||||||
tap(([request, about]) => {
|
tap(([request, about]) => {
|
||||||
this.queueService.viewContent(request.flowfileSummary, about.contentViewerUrl);
|
this.queueService.viewContent(request, about.contentViewerUrl);
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
{ dispatch: false }
|
{ dispatch: false }
|
||||||
|
|
|
@ -108,6 +108,16 @@
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Node Column -->
|
||||||
|
@if (displayedColumns.includes('node')) {
|
||||||
|
<ng-container matColumnDef="node">
|
||||||
|
<th mat-header-cell *matHeaderCellDef>Node</th>
|
||||||
|
<td mat-cell *matCellDef="let item" [title]="item.clusterNodeAddress">
|
||||||
|
{{ item.clusterNodeAddress }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Actions Column -->
|
<!-- Actions Column -->
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef></th>
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { RouterLink } from '@angular/router';
|
||||||
import { FlowFileSummary, ListingRequest } from '../../../state/queue-listing';
|
import { FlowFileSummary, ListingRequest } from '../../../state/queue-listing';
|
||||||
import { CurrentUser } from '../../../../../state/current-user';
|
import { CurrentUser } from '../../../../../state/current-user';
|
||||||
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
|
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
|
||||||
|
import { ClusterSummary } from '../../../../../state/cluster-summary';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'flowfile-table',
|
selector: 'flowfile-table',
|
||||||
|
@ -49,6 +50,20 @@ export class FlowFileTable {
|
||||||
this.destinationRunning = listingRequest.destinationRunning;
|
this.destinationRunning = listingRequest.destinationRunning;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Input() set clusterSummary(clusterSummary: ClusterSummary) {
|
||||||
|
if (clusterSummary?.connectedToCluster) {
|
||||||
|
// if we're connected to the cluster add a node column if it's not already present
|
||||||
|
if (!this.displayedColumns.includes('node')) {
|
||||||
|
this.displayedColumns.splice(this.displayedColumns.length - 1, 0, 'node');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if we're not connected to the cluster remove the node column if it is present
|
||||||
|
const nodeIndex = this.displayedColumns.indexOf('node');
|
||||||
|
if (nodeIndex > -1) {
|
||||||
|
this.displayedColumns.splice(nodeIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Input() currentUser!: CurrentUser;
|
@Input() currentUser!: CurrentUser;
|
||||||
@Input() contentViewerAvailable!: boolean;
|
@Input() contentViewerAvailable!: boolean;
|
||||||
|
@ -61,7 +76,6 @@ export class FlowFileTable {
|
||||||
protected readonly BulletinsTip = BulletinsTip;
|
protected readonly BulletinsTip = BulletinsTip;
|
||||||
protected readonly ValidationErrorsTip = ValidationErrorsTip;
|
protected readonly ValidationErrorsTip = ValidationErrorsTip;
|
||||||
|
|
||||||
// TODO - conditionally include the cluster column
|
|
||||||
displayedColumns: string[] = [
|
displayedColumns: string[] = [
|
||||||
'moreDetails',
|
'moreDetails',
|
||||||
'position',
|
'position',
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
[connectionLabel]="(connectionLabel$ | async)!"
|
[connectionLabel]="(connectionLabel$ | async)!"
|
||||||
[listingRequest]="listingRequest"
|
[listingRequest]="listingRequest"
|
||||||
[currentUser]="(currentUser$ | async)!"
|
[currentUser]="(currentUser$ | async)!"
|
||||||
|
[clusterSummary]="(clusterSummary$ | async)!"
|
||||||
[contentViewerAvailable]="contentViewerAvailable(about)"
|
[contentViewerAvailable]="contentViewerAvailable(about)"
|
||||||
(viewFlowFile)="viewFlowFile($event)"
|
(viewFlowFile)="viewFlowFile($event)"
|
||||||
(downloadContent)="downloadContent($event)"
|
(downloadContent)="downloadContent($event)"
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, OnDestroy } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { distinctUntilChanged, filter } from 'rxjs';
|
import { distinctUntilChanged, filter } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
@ -41,19 +41,22 @@ import { selectAbout } from '../../../../state/about/about.selectors';
|
||||||
import { About } from '../../../../state/about';
|
import { About } from '../../../../state/about';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { clearBannerErrors } from '../../../../state/error/error.actions';
|
import { clearBannerErrors } from '../../../../state/error/error.actions';
|
||||||
|
import { selectClusterSummary } from '../../../../state/cluster-summary/cluster-summary.selectors';
|
||||||
|
import { loadClusterSummary } from '../../../../state/cluster-summary/cluster-summary.actions';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'queue-listing',
|
selector: 'queue-listing',
|
||||||
templateUrl: './queue-listing.component.html',
|
templateUrl: './queue-listing.component.html',
|
||||||
styleUrls: ['./queue-listing.component.scss']
|
styleUrls: ['./queue-listing.component.scss']
|
||||||
})
|
})
|
||||||
export class QueueListing implements OnDestroy {
|
export class QueueListing implements OnInit, OnDestroy {
|
||||||
status$ = this.store.select(selectStatus);
|
status$ = this.store.select(selectStatus);
|
||||||
connectionLabel$ = this.store.select(selectConnectionLabel);
|
connectionLabel$ = this.store.select(selectConnectionLabel);
|
||||||
loadedTimestamp$ = this.store.select(selectLoadedTimestamp);
|
loadedTimestamp$ = this.store.select(selectLoadedTimestamp);
|
||||||
listingRequest$ = this.store.select(selectCompletedListingRequest);
|
listingRequest$ = this.store.select(selectCompletedListingRequest);
|
||||||
currentUser$ = this.store.select(selectCurrentUser);
|
currentUser$ = this.store.select(selectCurrentUser);
|
||||||
about$ = this.store.select(selectAbout);
|
about$ = this.store.select(selectAbout);
|
||||||
|
clusterSummary$ = this.store.select(selectClusterSummary);
|
||||||
|
|
||||||
constructor(private store: Store<NiFiState>) {
|
constructor(private store: Store<NiFiState>) {
|
||||||
this.store
|
this.store
|
||||||
|
@ -81,6 +84,10 @@ export class QueueListing implements OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.store.dispatch(loadClusterSummary());
|
||||||
|
}
|
||||||
|
|
||||||
refreshClicked(): void {
|
refreshClicked(): void {
|
||||||
this.store.dispatch(resubmitQueueListingRequest());
|
this.store.dispatch(resubmitQueueListingRequest());
|
||||||
}
|
}
|
||||||
|
@ -94,11 +101,25 @@ export class QueueListing implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadContent(flowfileSummary: FlowFileSummary): void {
|
downloadContent(flowfileSummary: FlowFileSummary): void {
|
||||||
this.store.dispatch(downloadFlowFileContent({ request: { flowfileSummary } }));
|
this.store.dispatch(
|
||||||
|
downloadFlowFileContent({
|
||||||
|
request: {
|
||||||
|
uri: flowfileSummary.uri,
|
||||||
|
clusterNodeId: flowfileSummary.clusterNodeId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewContent(flowfileSummary: FlowFileSummary): void {
|
viewContent(flowfileSummary: FlowFileSummary): void {
|
||||||
this.store.dispatch(viewFlowFileContent({ request: { flowfileSummary } }));
|
this.store.dispatch(
|
||||||
|
viewFlowFileContent({
|
||||||
|
request: {
|
||||||
|
uri: flowfileSummary.uri,
|
||||||
|
clusterNodeId: flowfileSummary.clusterNodeId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { ClusterSearchResults } from '../state/cluster-summary';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ClusterService {
|
||||||
|
private static readonly API: string = '../nifi-api';
|
||||||
|
|
||||||
|
constructor(private httpClient: HttpClient) {}
|
||||||
|
|
||||||
|
getClusterSummary(): Observable<any> {
|
||||||
|
return this.httpClient.get(`${ClusterService.API}/flow/cluster/summary`);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchCluster(q?: string): Observable<ClusterSearchResults> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
if (q) {
|
||||||
|
params = params.set('q', q);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.httpClient.get<ClusterSearchResults>(`${ClusterService.API}/flow/cluster/search-results`, {
|
||||||
|
params
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,4 +24,4 @@ export const loadAboutSuccess = createAction('[About] Load About Success', props
|
||||||
|
|
||||||
export const aboutApiError = createAction('[About] About Api Error', props<{ error: string }>());
|
export const aboutApiError = createAction('[About] About Api Error', props<{ error: string }>());
|
||||||
|
|
||||||
export const clearAboutApiError = createAction('[User] Clear About Api Error');
|
export const clearAboutApiError = createAction('[About] Clear About Api Error');
|
||||||
|
|
|
@ -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 { createAction, props } from '@ngrx/store';
|
||||||
|
import { LoadClusterSummaryResponse } from './index';
|
||||||
|
|
||||||
|
const CLUSTER_SUMMARY_STATE_PREFIX = '[Cluster Summary State]';
|
||||||
|
|
||||||
|
export const startClusterSummaryPolling = createAction(`${CLUSTER_SUMMARY_STATE_PREFIX} Start Cluster Summary Polling`);
|
||||||
|
|
||||||
|
export const stopClusterSummaryPolling = createAction(`${CLUSTER_SUMMARY_STATE_PREFIX} Stop Cluster Summary Polling`);
|
||||||
|
|
||||||
|
export const loadClusterSummary = createAction(`${CLUSTER_SUMMARY_STATE_PREFIX} Load Cluster Summary`);
|
||||||
|
|
||||||
|
export const loadClusterSummarySuccess = createAction(
|
||||||
|
`${CLUSTER_SUMMARY_STATE_PREFIX} Load Cluster Summary Success`,
|
||||||
|
props<{ response: LoadClusterSummaryResponse }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const clusterSummaryApiError = createAction(
|
||||||
|
`${CLUSTER_SUMMARY_STATE_PREFIX} Cluster Summary Api Error`,
|
||||||
|
props<{ error: string }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const clearClusterSummaryApiError = createAction(`${CLUSTER_SUMMARY_STATE_PREFIX} Clear About Api Error`);
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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 ClusterSummaryActions from './cluster-summary.actions';
|
||||||
|
import { asyncScheduler, catchError, from, interval, map, of, switchMap, takeUntil } from 'rxjs';
|
||||||
|
import { ClusterService } from '../../service/cluster.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ClusterSummaryEffects {
|
||||||
|
constructor(
|
||||||
|
private actions$: Actions,
|
||||||
|
private clusterService: ClusterService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
loadClusterSummary$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(ClusterSummaryActions.loadClusterSummary),
|
||||||
|
switchMap(() => {
|
||||||
|
return from(
|
||||||
|
this.clusterService.getClusterSummary().pipe(
|
||||||
|
map((response) =>
|
||||||
|
ClusterSummaryActions.loadClusterSummarySuccess({
|
||||||
|
response
|
||||||
|
})
|
||||||
|
),
|
||||||
|
catchError((error) => of(ClusterSummaryActions.clusterSummaryApiError({ error: error.error })))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
startProcessGroupPolling$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(ClusterSummaryActions.startClusterSummaryPolling),
|
||||||
|
switchMap(() =>
|
||||||
|
interval(30000, asyncScheduler).pipe(
|
||||||
|
takeUntil(this.actions$.pipe(ofType(ClusterSummaryActions.stopClusterSummaryPolling)))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
switchMap(() => of(ClusterSummaryActions.loadClusterSummary()))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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 { ClusterSummaryState } from './index';
|
||||||
|
import {
|
||||||
|
clusterSummaryApiError,
|
||||||
|
clearClusterSummaryApiError,
|
||||||
|
loadClusterSummary,
|
||||||
|
loadClusterSummarySuccess
|
||||||
|
} from './cluster-summary.actions';
|
||||||
|
|
||||||
|
export const initialState: ClusterSummaryState = {
|
||||||
|
clusterSummary: null,
|
||||||
|
error: null,
|
||||||
|
status: 'pending'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clusterSummaryReducer = createReducer(
|
||||||
|
initialState,
|
||||||
|
on(loadClusterSummary, (state) => ({
|
||||||
|
...state,
|
||||||
|
status: 'loading' as const
|
||||||
|
})),
|
||||||
|
on(loadClusterSummarySuccess, (state, { response }) => ({
|
||||||
|
...state,
|
||||||
|
clusterSummary: response.clusterSummary,
|
||||||
|
error: null,
|
||||||
|
status: 'success' as const
|
||||||
|
})),
|
||||||
|
on(clusterSummaryApiError, (state, { error }) => ({
|
||||||
|
...state,
|
||||||
|
error,
|
||||||
|
status: 'error' as const
|
||||||
|
})),
|
||||||
|
on(clearClusterSummaryApiError, (state) => ({
|
||||||
|
...state,
|
||||||
|
error: null,
|
||||||
|
status: 'pending' as const
|
||||||
|
}))
|
||||||
|
);
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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 { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||||
|
import { clusterSummaryFeatureKey, ClusterSummaryState } from './index';
|
||||||
|
|
||||||
|
export const selectClusterSummaryState = createFeatureSelector<ClusterSummaryState>(clusterSummaryFeatureKey);
|
||||||
|
|
||||||
|
export const selectClusterSummary = createSelector(
|
||||||
|
selectClusterSummaryState,
|
||||||
|
(state: ClusterSummaryState) => state.clusterSummary
|
||||||
|
);
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const clusterSummaryFeatureKey = 'clusterSummary';
|
||||||
|
|
||||||
|
export interface LoadClusterSummaryResponse {
|
||||||
|
clusterSummary: ClusterSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClusterSummary {
|
||||||
|
clustered: boolean;
|
||||||
|
connectedToCluster: boolean;
|
||||||
|
connectedNodes?: string;
|
||||||
|
connectedNodeCount: number;
|
||||||
|
totalNodeCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeSearchResult {
|
||||||
|
id: string;
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClusterSearchResults {
|
||||||
|
nodeResults: NodeSearchResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClusterSummaryState {
|
||||||
|
clusterSummary: ClusterSummary | null;
|
||||||
|
error: string | null;
|
||||||
|
status: 'pending' | 'loading' | 'error' | 'success';
|
||||||
|
}
|
|
@ -37,6 +37,8 @@ import { errorFeatureKey, ErrorState } from './error';
|
||||||
import { errorReducer } from './error/error.reducer';
|
import { errorReducer } from './error/error.reducer';
|
||||||
import { documentationFeatureKey, DocumentationState } from './documentation';
|
import { documentationFeatureKey, DocumentationState } from './documentation';
|
||||||
import { documentationReducer } from './documentation/documentation.reducer';
|
import { documentationReducer } from './documentation/documentation.reducer';
|
||||||
|
import { clusterSummaryFeatureKey, ClusterSummaryState } from './cluster-summary';
|
||||||
|
import { clusterSummaryReducer } from './cluster-summary/cluster-summary.reducer';
|
||||||
|
|
||||||
export interface NiFiState {
|
export interface NiFiState {
|
||||||
router: RouterReducerState;
|
router: RouterReducerState;
|
||||||
|
@ -50,6 +52,7 @@ export interface NiFiState {
|
||||||
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
|
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
|
||||||
[componentStateFeatureKey]: ComponentStateState;
|
[componentStateFeatureKey]: ComponentStateState;
|
||||||
[documentationFeatureKey]: DocumentationState;
|
[documentationFeatureKey]: DocumentationState;
|
||||||
|
[clusterSummaryFeatureKey]: ClusterSummaryState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rootReducers: ActionReducerMap<NiFiState> = {
|
export const rootReducers: ActionReducerMap<NiFiState> = {
|
||||||
|
@ -63,5 +66,6 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
|
||||||
[controllerServiceStateFeatureKey]: controllerServiceStateReducer,
|
[controllerServiceStateFeatureKey]: controllerServiceStateReducer,
|
||||||
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
|
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
|
||||||
[componentStateFeatureKey]: componentStateReducer,
|
[componentStateFeatureKey]: componentStateReducer,
|
||||||
[documentationFeatureKey]: documentationReducer
|
[documentationFeatureKey]: documentationReducer,
|
||||||
|
[clusterSummaryFeatureKey]: clusterSummaryReducer
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue