mirror of https://github.com/apache/nifi.git
[NIFI-12553] - Context menu option for View Status History (#8193)
* [NIFI-12553] - Context menu option for View Status History * Added View Status History to the canvas context menu for applicable component types * Added support for Node Status History from the flow menu * remove unused imports This closes #8193
This commit is contained in:
parent
a7c9eccf4a
commit
689f990978
|
@ -16,8 +16,6 @@
|
|||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { CanvasUtils } from './canvas-utils.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CanvasState } from '../state';
|
||||
|
@ -33,18 +31,17 @@ import {
|
|||
navigateToEditComponent,
|
||||
navigateToEditCurrentProcessGroup,
|
||||
navigateToProvenanceForComponent,
|
||||
navigateToViewStatusHistoryForComponent,
|
||||
reloadFlow,
|
||||
replayLastProvenanceEvent
|
||||
} from '../state/flow/flow.actions';
|
||||
import { ComponentType } from '../../../state/shared';
|
||||
import { DeleteComponentRequest, MoveComponentRequest } from '../state/flow';
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuDefinition,
|
||||
ContextMenuDefinitionProvider,
|
||||
ContextMenuItemDefinition
|
||||
} from '../../../ui/common/context-menu/context-menu.component';
|
||||
import { selection } from 'd3';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||
|
@ -550,14 +547,21 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
|||
isSeparator: true
|
||||
},
|
||||
{
|
||||
condition: function (canvasUtils: CanvasUtils, selection: any) {
|
||||
// TODO - supportsStats
|
||||
return false;
|
||||
condition: (canvasUtils: CanvasUtils, selection: any) => {
|
||||
return canvasUtils.canViewStatusHistory(selection);
|
||||
},
|
||||
clazz: 'fa fa-area-chart',
|
||||
text: 'View status history',
|
||||
action: function (store: Store<CanvasState>) {
|
||||
// TODO - showStats
|
||||
action: (store: Store<CanvasState>, selection: any) => {
|
||||
const selectionData = selection.datum();
|
||||
store.dispatch(
|
||||
navigateToViewStatusHistoryForComponent({
|
||||
request: {
|
||||
type: selectionData.type,
|
||||
id: selectionData.id
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -228,7 +228,7 @@ export class CanvasUtils {
|
|||
// get the selection data
|
||||
const selectionData: any = selection.datum();
|
||||
|
||||
let supportsModification: boolean = false;
|
||||
let supportsModification = false;
|
||||
if (this.isProcessor(selection) || this.isInputPort(selection) || this.isOutputPort(selection)) {
|
||||
supportsModification = !(
|
||||
selectionData.status.aggregateSnapshot.runStatus === 'Running' ||
|
||||
|
@ -246,8 +246,8 @@ export class CanvasUtils {
|
|||
} else if (this.isLabel(selection)) {
|
||||
supportsModification = true;
|
||||
} else if (this.isConnection(selection)) {
|
||||
let isSourceConfigurable: boolean = false;
|
||||
let isDestinationConfigurable: boolean = false;
|
||||
let isSourceConfigurable = false;
|
||||
let isDestinationConfigurable = false;
|
||||
|
||||
const sourceComponentId: string = this.getConnectionSourceComponentId(selectionData);
|
||||
const source: any = d3.select('#id-' + sourceComponentId);
|
||||
|
@ -286,7 +286,7 @@ export class CanvasUtils {
|
|||
}
|
||||
|
||||
const self: CanvasUtils = this;
|
||||
let isDeletable: boolean = true;
|
||||
let isDeletable = true;
|
||||
selection.each(function (this: any) {
|
||||
if (!self.isDeletable(d3.select(this))) {
|
||||
isDeletable = false;
|
||||
|
@ -487,6 +487,24 @@ export class CanvasUtils {
|
|||
return this.isProcessor(selection) && this.canAccessProvenance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current user can view the status history for the selected component.
|
||||
*
|
||||
* @param {selection} selection
|
||||
*/
|
||||
public canViewStatusHistory(selection: any): boolean {
|
||||
if (selection.size() !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.isProcessor(selection) ||
|
||||
this.isConnection(selection) ||
|
||||
this.isRemoteProcessGroup(selection) ||
|
||||
this.isProcessGroup(selection)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently selected components and connections.
|
||||
*
|
||||
|
@ -639,7 +657,7 @@ export class CanvasUtils {
|
|||
const connections: Map<string, any> = new Map<string, any>();
|
||||
const components: Map<string, any> = new Map<string, any>();
|
||||
|
||||
let isDisconnected: boolean = true;
|
||||
let isDisconnected = true;
|
||||
|
||||
// include connections
|
||||
selection
|
||||
|
@ -894,7 +912,7 @@ export class CanvasUtils {
|
|||
* @param tooltipData
|
||||
*/
|
||||
public canvasTooltip<C>(viewContainerRef: ViewContainerRef, type: Type<C>, selection: any, tooltipData: any): void {
|
||||
let closeTimer: number = -1;
|
||||
let closeTimer = -1;
|
||||
let tooltipRef: ComponentRef<C> | undefined;
|
||||
|
||||
selection
|
||||
|
@ -1034,7 +1052,7 @@ export class CanvasUtils {
|
|||
* @param {string} cacheName
|
||||
*/
|
||||
public multilineEllipsis(selection: any, lineCount: number, text: string, cacheName: string) {
|
||||
let i: number = 1;
|
||||
let i = 1;
|
||||
const words: string[] = text.split(/\s+/).reverse();
|
||||
|
||||
// get the appropriate position
|
||||
|
@ -1047,7 +1065,7 @@ export class CanvasUtils {
|
|||
|
||||
// go through each word
|
||||
let word = words.pop();
|
||||
while (!!word) {
|
||||
while (word) {
|
||||
// add the current word
|
||||
line.push(word);
|
||||
|
||||
|
@ -1069,7 +1087,7 @@ export class CanvasUtils {
|
|||
if (++i >= lineCount) {
|
||||
// get the remainder using the current word and
|
||||
// reversing whats left
|
||||
var remainder = [word].concat(words.reverse());
|
||||
const remainder = [word].concat(words.reverse());
|
||||
|
||||
// apply ellipsis to the last line
|
||||
this.ellipsis(tspan, remainder.join(' '), cacheName);
|
||||
|
@ -1103,7 +1121,7 @@ export class CanvasUtils {
|
|||
// if there is active threads show the count, otherwise hide
|
||||
if (activeThreads > 0 || terminatedThreads > 0) {
|
||||
const generateThreadsTip = function () {
|
||||
var tip = activeThreads + ' active threads';
|
||||
let tip = activeThreads + ' active threads';
|
||||
if (terminatedThreads > 0) {
|
||||
tip += ' (' + terminatedThreads + ' terminated)';
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ import {
|
|||
NavigateToControllerServicesRequest,
|
||||
ReplayLastProvenanceEventRequest
|
||||
} from './index';
|
||||
import { StatusHistoryRequest } from '../../../../state/status-history';
|
||||
|
||||
/*
|
||||
Loading Flow
|
||||
|
@ -237,6 +238,15 @@ export const createComponentComplete = createAction(
|
|||
props<{ response: CreateComponentResponse }>()
|
||||
);
|
||||
|
||||
export const navigateToViewStatusHistoryForComponent = createAction(
|
||||
'[Canvas] Navigate To Status History For Component',
|
||||
props<{ request: OpenComponentDialogRequest }>()
|
||||
);
|
||||
|
||||
export const viewStatusHistoryForComponent = createAction(
|
||||
'[Canvas] View Status History for Component',
|
||||
props<{ request: StatusHistoryRequest }>()
|
||||
);
|
||||
/*
|
||||
Update Component Actions
|
||||
*/
|
||||
|
|
|
@ -20,6 +20,7 @@ import { FlowService } from '../../service/flow.service';
|
|||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import * as FlowActions from './flow.actions';
|
||||
import * as ParameterActions from '../parameter/parameter.actions';
|
||||
import * as StatusHistoryActions from '../../../../state/status-history/status-history.actions';
|
||||
import {
|
||||
asyncScheduler,
|
||||
catchError,
|
||||
|
@ -673,6 +674,49 @@ export class FlowEffects {
|
|||
{ dispatch: false }
|
||||
);
|
||||
|
||||
navigateToViewStatusHistoryForComponent$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowActions.navigateToViewStatusHistoryForComponent),
|
||||
map((action) => action.request),
|
||||
withLatestFrom(this.store.select(selectCurrentProcessGroupId)),
|
||||
tap(([request, currentProcessGroupId]) => {
|
||||
this.router.navigate([
|
||||
'/process-groups',
|
||||
currentProcessGroupId,
|
||||
request.type,
|
||||
request.id,
|
||||
'history'
|
||||
]);
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
completeStatusHistoryForComponent$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(StatusHistoryActions.viewStatusHistoryComplete),
|
||||
map((action) => action.request),
|
||||
filter((request) => request.source === 'canvas'),
|
||||
tap((request) => {
|
||||
this.store.dispatch(
|
||||
FlowActions.selectComponents({
|
||||
request: {
|
||||
components: [
|
||||
{
|
||||
id: request.componentId,
|
||||
componentType: request.componentType
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
navigateToControllerServicesForProcessGroup$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
|
@ -1943,8 +1987,8 @@ export class FlowEffects {
|
|||
})
|
||||
);
|
||||
} else if (response.nodeSnapshots) {
|
||||
let replayedCount: number = 0;
|
||||
let unavailableCount: number = 0;
|
||||
let replayedCount = 0;
|
||||
let unavailableCount = 0;
|
||||
|
||||
response.nodeSnapshots.forEach((nodeResponse: any) => {
|
||||
if (nodeResponse.snapshot.eventAvailable) {
|
||||
|
|
|
@ -121,6 +121,19 @@ export const selectEditedCurrentProcessGroup = createSelector(selectCurrentRoute
|
|||
return null;
|
||||
});
|
||||
|
||||
export const selectViewStatusHistoryComponent = createSelector(selectCurrentRoute, (route) => {
|
||||
let selectedComponent: SelectedComponent | null = null;
|
||||
if (route?.routeConfig?.path == 'history') {
|
||||
if (route.params.id && route.params.type) {
|
||||
selectedComponent = {
|
||||
id: route.params.id,
|
||||
componentType: route.params.type
|
||||
};
|
||||
}
|
||||
}
|
||||
return selectedComponent;
|
||||
});
|
||||
|
||||
export const selectTransitionRequired = createSelector(selectFlowState, (state: FlowState) => state.transitionRequired);
|
||||
|
||||
export const selectDragging = createSelector(selectFlowState, (state: FlowState) => state.dragging);
|
||||
|
|
|
@ -29,7 +29,10 @@ const routes: Routes = [
|
|||
{
|
||||
path: ':type/:id',
|
||||
component: Canvas,
|
||||
children: [{ path: 'edit', component: Canvas }]
|
||||
children: [
|
||||
{ path: 'edit', component: Canvas },
|
||||
{ path: 'history', component: Canvas }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -53,7 +53,8 @@ import {
|
|||
selectRemoteProcessGroup,
|
||||
selectSingleEditedComponent,
|
||||
selectSingleSelectedComponent,
|
||||
selectSkipTransform
|
||||
selectSkipTransform,
|
||||
selectViewStatusHistoryComponent
|
||||
} from '../../state/flow/flow.selectors';
|
||||
import { filter, map, switchMap, take, withLatestFrom } from 'rxjs';
|
||||
import { restoreViewport, zoomFit } from '../../state/transform/transform.actions';
|
||||
|
@ -61,6 +62,7 @@ import { ComponentType } from '../../../../state/shared';
|
|||
import { initialState } from '../../state/flow/flow.reducer';
|
||||
import { ContextMenuDefinitionProvider } from '../../../../ui/common/context-menu/context-menu.component';
|
||||
import { CanvasContextMenu } from '../../service/canvas-context-menu.service';
|
||||
import { getStatusHistoryAndOpenDialog } from '../../../../state/status-history/status-history.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'fd-canvas',
|
||||
|
@ -72,7 +74,7 @@ export class Canvas implements OnInit, OnDestroy {
|
|||
private canvas: any;
|
||||
|
||||
private scale: number = INITIAL_SCALE;
|
||||
private canvasClicked: boolean = false;
|
||||
private canvasClicked = false;
|
||||
|
||||
constructor(
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
|
@ -239,6 +241,28 @@ export class Canvas implements OnInit, OnDestroy {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
this.store
|
||||
.select(selectCurrentProcessGroupId)
|
||||
.pipe(
|
||||
filter((processGroupId) => processGroupId != initialState.id),
|
||||
switchMap(() => this.store.select(selectViewStatusHistoryComponent)),
|
||||
filter((selectedComponent) => selectedComponent != null),
|
||||
takeUntilDestroyed()
|
||||
)
|
||||
.subscribe((component) => {
|
||||
if (component) {
|
||||
this.store.dispatch(
|
||||
getStatusHistoryAndOpenDialog({
|
||||
request: {
|
||||
source: 'canvas',
|
||||
componentType: component.componentType,
|
||||
componentId: component.id
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
<i class="fa fa-fw fa-history mr-2"></i>
|
||||
Flow Configuration History
|
||||
</button>
|
||||
<button mat-menu-item class="global-menu-item">
|
||||
<button mat-menu-item class="global-menu-item" (click)="viewNodeStatusHistory()">
|
||||
<i class="fa fa-fw fa-area-chart mr-2"></i>
|
||||
Node Status History
|
||||
</button>
|
||||
|
|
|
@ -38,6 +38,7 @@ import { AsyncPipe, NgIf, NgOptimizedImage } from '@angular/common';
|
|||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { FlowStatus } from './flow-status/flow-status.component';
|
||||
import * as StatusHistoryActions from '../../../../../state/status-history/status-history.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'fd-header',
|
||||
|
@ -84,4 +85,14 @@ export class HeaderComponent {
|
|||
logout(): void {
|
||||
this.authService.logout();
|
||||
}
|
||||
|
||||
viewNodeStatusHistory(): void {
|
||||
this.store.dispatch(
|
||||
StatusHistoryActions.getNodeStatusHistoryAndOpenDialog({
|
||||
request: {
|
||||
source: 'menu'
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export class StatusHistoryService {
|
|||
private client: Client
|
||||
) {}
|
||||
|
||||
getProcessorStatusHistory(componentType: ComponentType, componentId: string) {
|
||||
getComponentStatusHistory(componentType: ComponentType, componentId: string) {
|
||||
let componentPath: string;
|
||||
switch (componentType) {
|
||||
case ComponentType.Processor:
|
||||
|
@ -51,4 +51,8 @@ export class StatusHistoryService {
|
|||
`${StatusHistoryService.API}/flow/${componentPath}/${encodeURIComponent(componentId)}/status/history`
|
||||
);
|
||||
}
|
||||
|
||||
getNodeStatusHistory() {
|
||||
return this.httpClient.get(`${StatusHistoryService.API}/controller/status/history`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,10 @@ export interface StatusHistoryRequest {
|
|||
componentType: ComponentType;
|
||||
}
|
||||
|
||||
export interface NodeStatusHistoryRequest {
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface FieldDescriptor {
|
||||
description: string;
|
||||
field: string;
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
*/
|
||||
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { StatusHistoryRequest, StatusHistoryResponse } from './index';
|
||||
import { NodeStatusHistoryRequest, StatusHistoryRequest, StatusHistoryResponse } from './index';
|
||||
|
||||
const STATUS_HISTORY_PREFIX: string = '[Status History]';
|
||||
const STATUS_HISTORY_PREFIX = '[Status History]';
|
||||
|
||||
export const reloadStatusHistory = createAction(
|
||||
`${STATUS_HISTORY_PREFIX} Reload Status History`,
|
||||
|
@ -30,6 +30,11 @@ export const getStatusHistoryAndOpenDialog = createAction(
|
|||
props<{ request: StatusHistoryRequest }>()
|
||||
);
|
||||
|
||||
export const getNodeStatusHistoryAndOpenDialog = createAction(
|
||||
`${STATUS_HISTORY_PREFIX} Get Node Status History and Open Dialog`,
|
||||
props<{ request: NodeStatusHistoryRequest }>()
|
||||
);
|
||||
|
||||
export const reloadStatusHistorySuccess = createAction(
|
||||
`${STATUS_HISTORY_PREFIX} Reload Status History Success`,
|
||||
props<{ response: StatusHistoryResponse }>()
|
||||
|
@ -37,12 +42,12 @@ export const reloadStatusHistorySuccess = createAction(
|
|||
|
||||
export const loadStatusHistorySuccess = createAction(
|
||||
`${STATUS_HISTORY_PREFIX} Load Status History Success`,
|
||||
props<{ request: StatusHistoryRequest; response: StatusHistoryResponse }>()
|
||||
props<{ request: StatusHistoryRequest | NodeStatusHistoryRequest; response: StatusHistoryResponse }>()
|
||||
);
|
||||
|
||||
export const openStatusHistoryDialog = createAction(
|
||||
`${STATUS_HISTORY_PREFIX} Open Status History Dialog`,
|
||||
props<{ request: StatusHistoryRequest }>()
|
||||
props<{ request: StatusHistoryRequest | NodeStatusHistoryRequest }>()
|
||||
);
|
||||
|
||||
export const statusHistoryApiError = createAction(
|
||||
|
@ -56,3 +61,8 @@ export const viewStatusHistoryComplete = createAction(
|
|||
`${STATUS_HISTORY_PREFIX} View Status History Complete`,
|
||||
props<{ request: StatusHistoryRequest }>()
|
||||
);
|
||||
|
||||
export const viewNodeStatusHistoryComplete = createAction(
|
||||
`${STATUS_HISTORY_PREFIX} View Node Status History Complete`,
|
||||
props<{ request: NodeStatusHistoryRequest }>()
|
||||
);
|
||||
|
|
|
@ -43,7 +43,7 @@ export class StatusHistoryEffects {
|
|||
switchMap((request: StatusHistoryRequest) =>
|
||||
from(
|
||||
this.statusHistoryService
|
||||
.getProcessorStatusHistory(request.componentType, request.componentId)
|
||||
.getComponentStatusHistory(request.componentType, request.componentId)
|
||||
.pipe(
|
||||
map((response: any) =>
|
||||
StatusHistoryActions.reloadStatusHistorySuccess({
|
||||
|
@ -75,7 +75,7 @@ export class StatusHistoryEffects {
|
|||
switchMap((request) =>
|
||||
from(
|
||||
this.statusHistoryService
|
||||
.getProcessorStatusHistory(request.componentType, request.componentId)
|
||||
.getComponentStatusHistory(request.componentType, request.componentId)
|
||||
.pipe(
|
||||
map((response: any) =>
|
||||
StatusHistoryActions.loadStatusHistorySuccess({
|
||||
|
@ -101,6 +101,37 @@ export class StatusHistoryEffects {
|
|||
)
|
||||
);
|
||||
|
||||
getNodeStatusHistoryAndOpenDialog$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(StatusHistoryActions.getNodeStatusHistoryAndOpenDialog),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(
|
||||
this.statusHistoryService.getNodeStatusHistory().pipe(
|
||||
map((response: any) =>
|
||||
StatusHistoryActions.loadStatusHistorySuccess({
|
||||
request,
|
||||
response: {
|
||||
statusHistory: {
|
||||
canRead: response.canRead,
|
||||
statusHistory: response.statusHistory
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
StatusHistoryActions.statusHistoryApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
loadStatusHistorySuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(StatusHistoryActions.loadStatusHistorySuccess),
|
||||
|
@ -122,15 +153,25 @@ export class StatusHistoryEffects {
|
|||
|
||||
dialogReference.afterClosed().subscribe((response) => {
|
||||
if (response !== 'ROUTED') {
|
||||
this.store.dispatch(
|
||||
StatusHistoryActions.viewStatusHistoryComplete({
|
||||
request: {
|
||||
source: request.source,
|
||||
componentType: request.componentType,
|
||||
componentId: request.componentId
|
||||
}
|
||||
})
|
||||
);
|
||||
if ('componentType' in request) {
|
||||
this.store.dispatch(
|
||||
StatusHistoryActions.viewStatusHistoryComplete({
|
||||
request: {
|
||||
source: request.source,
|
||||
componentType: request.componentType,
|
||||
componentId: request.componentId
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
StatusHistoryActions.viewNodeStatusHistoryComplete({
|
||||
request: {
|
||||
source: request.source
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue