mirror of https://github.com/apache/nifi.git
NIFI-12737: Removing all transitions when updating the Canvas transform (#8355)
* NIFI-12737: - Recording last canvas URL in local storage. - Using last canvas URL in global menu Canvas item. - Conditionally applying transitions when centering components. - Always applying transitions during zoom events (1:1, fit, zoom in/out). - Adding support to center more than one component. * NIFI-12737: - Fixing bug when attempting to click on the search result of the currently selected component. - Handling centering of a single selection different from a bulk selection as it performs betters with Connections. This closes #8355
This commit is contained in:
parent
91f339bf0f
commit
13c70c0f30
|
@ -20,7 +20,7 @@ import { CanvasUtils } from './canvas-utils.service';
|
|||
import { Store } from '@ngrx/store';
|
||||
import { CanvasState } from '../state';
|
||||
import {
|
||||
centerSelectedComponent,
|
||||
centerSelectedComponents,
|
||||
deleteComponents,
|
||||
enterProcessGroup,
|
||||
getParameterContextsAndOpenGroupComponentsDialog,
|
||||
|
@ -928,12 +928,12 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
|||
},
|
||||
{
|
||||
condition: (selection: any) => {
|
||||
return selection.size() === 1 && !this.canvasUtils.isConnection(selection);
|
||||
return !selection.empty();
|
||||
},
|
||||
clazz: 'fa fa-crosshairs',
|
||||
text: 'Center in view',
|
||||
action: () => {
|
||||
this.store.dispatch(centerSelectedComponent());
|
||||
this.store.dispatch(centerSelectedComponents({ request: { allowTransition: true } }));
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -590,6 +590,28 @@ export class CanvasUtils {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position for centering a connection based on the presence of bends.
|
||||
*
|
||||
* @param d connection data
|
||||
*/
|
||||
public getPositionForCenteringConnection(d: any): Position {
|
||||
let x, y;
|
||||
if (d.bends.length > 0) {
|
||||
const i: number = Math.min(Math.max(0, d.labelIndex), d.bends.length - 1);
|
||||
x = d.bends[i].x;
|
||||
y = d.bends[i].y;
|
||||
} else {
|
||||
x = (d.start.x + d.end.x) / 2;
|
||||
y = (d.start.y + d.end.y) / 2;
|
||||
}
|
||||
|
||||
return {
|
||||
x,
|
||||
y
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the component id of the source of this processor. If the connection is attached
|
||||
* to a port in a [sub|remote] group, the component id will be that of the group. Otherwise
|
||||
|
@ -1484,6 +1506,7 @@ export class CanvasUtils {
|
|||
});
|
||||
return canStopTransmitting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the components in the specified selection can be operated.
|
||||
*
|
||||
|
|
|
@ -51,6 +51,7 @@ export class CanvasView {
|
|||
private behavior: any;
|
||||
|
||||
private birdseyeTranslateInProgress = false;
|
||||
private allowTransition = false;
|
||||
|
||||
constructor(
|
||||
private store: Store<CanvasState>,
|
||||
|
@ -85,6 +86,10 @@ export class CanvasView {
|
|||
this.svg = svg;
|
||||
this.canvas = canvas;
|
||||
|
||||
this.k = INITIAL_SCALE;
|
||||
this.x = INITIAL_TRANSLATE.x;
|
||||
this.y = INITIAL_TRANSLATE.y;
|
||||
|
||||
this.labelManager.init();
|
||||
this.funnelManager.init();
|
||||
this.portManager.init(viewContainerRef);
|
||||
|
@ -118,7 +123,7 @@ export class CanvasView {
|
|||
|
||||
// refresh the canvas
|
||||
refreshed = self.refresh({
|
||||
transition: self.shouldTransition(event.sourceEvent),
|
||||
transition: self.shouldTransition(),
|
||||
refreshComponents: false,
|
||||
refreshBirdseye: false
|
||||
});
|
||||
|
@ -170,18 +175,73 @@ export class CanvasView {
|
|||
return this.birdseyeTranslateInProgress;
|
||||
}
|
||||
|
||||
// see if the scale has changed during this zoom event,
|
||||
// we want to only transition when zooming in/out as running
|
||||
// the transitions during pan events is undesirable
|
||||
private shouldTransition(sourceEvent: any): boolean {
|
||||
private shouldTransition(): boolean {
|
||||
if (this.birdseyeTranslateInProgress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceEvent) {
|
||||
return sourceEvent.type === 'wheel' || sourceEvent.type === 'mousewheel';
|
||||
return this.allowTransition;
|
||||
}
|
||||
|
||||
public isSelectedComponentOnScreen(): boolean {
|
||||
const canvasContainer: any = document.getElementById('canvas-container');
|
||||
|
||||
if (canvasContainer == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selection: any = this.canvasUtils.getSelection();
|
||||
if (selection.size() !== 1) {
|
||||
return false;
|
||||
}
|
||||
const d = selection.datum();
|
||||
|
||||
let translate = [this.x, this.y];
|
||||
const scale = this.k;
|
||||
|
||||
// scale the translation
|
||||
translate = [translate[0] / scale, translate[1] / scale];
|
||||
|
||||
// get the normalized screen width and height
|
||||
const screenWidth = canvasContainer.offsetWidth / scale;
|
||||
const screenHeight = canvasContainer.offsetHeight / scale;
|
||||
|
||||
// calculate the screen bounds one screens worth in each direction
|
||||
const screenLeft = -translate[0];
|
||||
const screenTop = -translate[1];
|
||||
const screenRight = screenLeft + screenWidth;
|
||||
const screenBottom = screenTop + screenHeight;
|
||||
|
||||
if (this.canvasUtils.isConnection(selection)) {
|
||||
let connectionX, connectionY;
|
||||
if (d.bends.length > 0) {
|
||||
const i: number = Math.min(Math.max(0, d.labelIndex), d.bends.length - 1);
|
||||
connectionX = d.bends[i].x;
|
||||
connectionY = d.bends[i].y;
|
||||
} else {
|
||||
connectionX = (d.start.x + d.end.x) / 2;
|
||||
connectionY = (d.start.y + d.end.y) / 2;
|
||||
}
|
||||
|
||||
return (
|
||||
screenLeft < connectionX &&
|
||||
screenRight > connectionX &&
|
||||
screenTop < connectionY &&
|
||||
screenBottom > connectionY
|
||||
);
|
||||
} else {
|
||||
return true;
|
||||
const componentLeft: number = d.position.x;
|
||||
const componentTop: number = d.position.y;
|
||||
const componentRight: number = componentLeft + d.dimensions.width;
|
||||
const componentBottom: number = componentTop + d.dimensions.height;
|
||||
|
||||
// determine if the component is now visible
|
||||
return (
|
||||
screenLeft < componentRight &&
|
||||
screenRight > componentLeft &&
|
||||
screenTop < componentBottom &&
|
||||
screenBottom > componentTop
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,15 +290,7 @@ export class CanvasView {
|
|||
return false;
|
||||
}
|
||||
|
||||
let x, y;
|
||||
if (d.bends.length > 0) {
|
||||
const i: number = Math.min(Math.max(0, d.labelIndex), d.bends.length - 1);
|
||||
x = d.bends[i].x;
|
||||
y = d.bends[i].y;
|
||||
} else {
|
||||
x = (d.start.x + d.end.x) / 2;
|
||||
y = (d.start.y + d.end.y) / 2;
|
||||
}
|
||||
const { x, y } = self.canvasUtils.getPositionForCenteringConnection(d);
|
||||
|
||||
return screenLeft < x && screenRight > x && screenTop < y && screenBottom > y;
|
||||
};
|
||||
|
@ -293,7 +345,7 @@ export class CanvasView {
|
|||
}
|
||||
|
||||
/**
|
||||
* Whether or not a component should be rendered based solely on the current scale.
|
||||
* Whether a component should be rendered based solely on the current scale.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
|
@ -301,45 +353,93 @@ export class CanvasView {
|
|||
return this.k >= CanvasView.MIN_SCALE_TO_RENDER;
|
||||
}
|
||||
|
||||
public centerSelectedComponent(): void {
|
||||
const selection: any = this.canvasUtils.getSelection();
|
||||
if (selection.size() === 1) {
|
||||
let box;
|
||||
if (this.canvasUtils.isConnection(selection)) {
|
||||
let x, y;
|
||||
const d = selection.datum();
|
||||
|
||||
// get the position of the connection label
|
||||
if (d.bends.length > 0) {
|
||||
const i: number = Math.min(Math.max(0, d.labelIndex), d.bends.length - 1);
|
||||
x = d.bends[i].x;
|
||||
y = d.bends[i].y;
|
||||
} else {
|
||||
x = (d.start.x + d.end.x) / 2;
|
||||
y = (d.start.y + d.end.y) / 2;
|
||||
}
|
||||
|
||||
box = {
|
||||
x: x,
|
||||
y: y,
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
} else {
|
||||
const selectionData = selection.datum();
|
||||
const selectionPosition = selectionData.position;
|
||||
|
||||
box = {
|
||||
x: selectionPosition.x,
|
||||
y: selectionPosition.y,
|
||||
width: selectionData.dimensions.width,
|
||||
height: selectionData.dimensions.height
|
||||
};
|
||||
}
|
||||
|
||||
// center on the component
|
||||
this.centerBoundingBox(box);
|
||||
public centerSelectedComponents(allowTransition: boolean): void {
|
||||
const canvasContainer: any = document.getElementById('canvas-container');
|
||||
if (canvasContainer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection: any = this.canvasUtils.getSelection();
|
||||
if (selection.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let bbox;
|
||||
if (selection.size() === 1) {
|
||||
bbox = this.getSingleSelectionBoundingClientRect(selection);
|
||||
} else {
|
||||
bbox = this.getBulkSelectionBoundingClientRect(selection, canvasContainer);
|
||||
}
|
||||
|
||||
this.allowTransition = allowTransition;
|
||||
this.centerBoundingBox(bbox);
|
||||
this.allowTransition = false;
|
||||
}
|
||||
|
||||
private getSingleSelectionBoundingClientRect(selection: any): any {
|
||||
let bbox;
|
||||
if (this.canvasUtils.isConnection(selection)) {
|
||||
const d = selection.datum();
|
||||
|
||||
// get the position of the connection label
|
||||
const { x, y } = this.canvasUtils.getPositionForCenteringConnection(d);
|
||||
|
||||
bbox = {
|
||||
x: x,
|
||||
y: y,
|
||||
width: 1,
|
||||
height: 1
|
||||
};
|
||||
} else {
|
||||
const selectionData = selection.datum();
|
||||
const selectionPosition = selectionData.position;
|
||||
|
||||
bbox = {
|
||||
x: selectionPosition.x,
|
||||
y: selectionPosition.y,
|
||||
width: selectionData.dimensions.width,
|
||||
height: selectionData.dimensions.height
|
||||
};
|
||||
}
|
||||
|
||||
return bbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a BoundingClientRect, normalized to the canvas, that encompasses all nodes in a given selection.
|
||||
*/
|
||||
private getBulkSelectionBoundingClientRect(selection: any, canvasContainer: any): any {
|
||||
const canvasBoundingBox: any = canvasContainer.getBoundingClientRect();
|
||||
|
||||
const initialBBox: any = {
|
||||
x: Number.MAX_VALUE,
|
||||
y: Number.MAX_VALUE,
|
||||
right: Number.MIN_VALUE,
|
||||
bottom: Number.MIN_VALUE
|
||||
};
|
||||
|
||||
const bbox = selection.nodes().reduce((aggregateBBox: any, node: any) => {
|
||||
const rect = node.getBoundingClientRect();
|
||||
aggregateBBox.x = Math.min(rect.x, aggregateBBox.x);
|
||||
aggregateBBox.y = Math.min(rect.y, aggregateBBox.y);
|
||||
aggregateBBox.right = Math.max(rect.right, aggregateBBox.right);
|
||||
aggregateBBox.bottom = Math.max(rect.bottom, aggregateBBox.bottom);
|
||||
|
||||
return aggregateBBox;
|
||||
}, initialBBox);
|
||||
|
||||
// normalize the bounding box with scale and translate
|
||||
bbox.x = (bbox.x - this.x) / this.k;
|
||||
bbox.y = (bbox.y - canvasBoundingBox.top - this.y) / this.k;
|
||||
bbox.right = (bbox.right - this.x) / this.k;
|
||||
bbox.bottom = (bbox.bottom - canvasBoundingBox.top - this.y) / this.k;
|
||||
|
||||
bbox.width = bbox.right - bbox.x;
|
||||
bbox.height = bbox.bottom - bbox.y;
|
||||
bbox.top = bbox.y;
|
||||
bbox.left = bbox.x;
|
||||
|
||||
return bbox;
|
||||
}
|
||||
|
||||
private centerBoundingBox(boundingBox: any): void {
|
||||
|
@ -430,14 +530,18 @@ export class CanvasView {
|
|||
* Zooms in a single zoom increment.
|
||||
*/
|
||||
public zoomIn(): void {
|
||||
this.allowTransition = true;
|
||||
this.scale(CanvasView.INCREMENT);
|
||||
this.allowTransition = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms out a single zoom increment.
|
||||
*/
|
||||
public zoomOut(): void {
|
||||
this.allowTransition = true;
|
||||
this.scale(1 / CanvasView.INCREMENT);
|
||||
this.allowTransition = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -476,7 +580,7 @@ export class CanvasView {
|
|||
graphTop -= 50;
|
||||
}
|
||||
|
||||
// center as appropriate
|
||||
this.allowTransition = true;
|
||||
this.centerBoundingBox({
|
||||
x: graphLeft - translate[0] / scale,
|
||||
y: graphTop - translate[1] / scale,
|
||||
|
@ -484,6 +588,7 @@ export class CanvasView {
|
|||
height: canvasHeight / newScale,
|
||||
scale: newScale
|
||||
});
|
||||
this.allowTransition = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -530,8 +635,9 @@ export class CanvasView {
|
|||
};
|
||||
}
|
||||
|
||||
// center as appropriate
|
||||
this.allowTransition = true;
|
||||
this.centerBoundingBox(box);
|
||||
this.allowTransition = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -109,15 +109,7 @@ export class ConnectionManager {
|
|||
private getLabelPosition(connectionLabel: any): Position {
|
||||
const d = connectionLabel.datum();
|
||||
|
||||
let x, y;
|
||||
if (d.bends.length > 0) {
|
||||
const i: number = Math.min(Math.max(0, d.labelIndex), d.bends.length - 1);
|
||||
x = d.bends[i].x;
|
||||
y = d.bends[i].y;
|
||||
} else {
|
||||
x = (d.start.x + d.end.x) / 2;
|
||||
y = (d.start.y + d.end.y) / 2;
|
||||
}
|
||||
let { x, y } = this.canvasUtils.getPositionForCenteringConnection(d);
|
||||
|
||||
// offset to account for the label dimensions
|
||||
x -= ConnectionManager.DIMENSIONS.width / 2;
|
||||
|
|
|
@ -70,7 +70,8 @@ import {
|
|||
UploadProcessGroupRequest,
|
||||
NavigateToQueueListing,
|
||||
StartProcessGroupResponse,
|
||||
StopProcessGroupResponse
|
||||
StopProcessGroupResponse,
|
||||
CenterComponentRequest
|
||||
} from './index';
|
||||
import { StatusHistoryRequest } from '../../../../state/status-history';
|
||||
|
||||
|
@ -186,7 +187,10 @@ export const removeSelectedComponents = createAction(
|
|||
props<{ request: SelectComponentsRequest }>()
|
||||
);
|
||||
|
||||
export const centerSelectedComponent = createAction(`${CANVAS_PREFIX} Center Selected Component`);
|
||||
export const centerSelectedComponents = createAction(
|
||||
`${CANVAS_PREFIX} Center Selected Components`,
|
||||
props<{ request: CenterComponentRequest }>()
|
||||
);
|
||||
|
||||
/*
|
||||
Create Component Actions
|
||||
|
@ -425,11 +429,27 @@ export const setTransitionRequired = createAction(
|
|||
props<{ transitionRequired: boolean }>()
|
||||
);
|
||||
|
||||
/**
|
||||
* skipTransform is used when handling URL events for loading the current PG and component [bulk] selection. since the
|
||||
* URL is the source of truth we need to indicate skipTransform when the URL changes based on the user selection on
|
||||
* the canvas. However, we do not want the transform skipped when using link to open or a particular part of the flow.
|
||||
* In these cases, we want the transform to be applied so the viewport is restored or the component(s) is centered.
|
||||
*/
|
||||
export const setSkipTransform = createAction(
|
||||
`${CANVAS_PREFIX} Set Skip Transform`,
|
||||
props<{ skipTransform: boolean }>()
|
||||
);
|
||||
|
||||
/**
|
||||
* allowTransition is a flag that can be set that indicates if a transition should be used when applying a transform.
|
||||
* By default, restoring the viewport or selecting/centering components will not use a transition unless explicitly
|
||||
* specified. Zoom based transforms (like fit or 1:1) will always use a transition.
|
||||
*/
|
||||
export const setAllowTransition = createAction(
|
||||
`${CANVAS_PREFIX} Set Allow Transition`,
|
||||
props<{ allowTransition: boolean }>()
|
||||
);
|
||||
|
||||
export const navigateToComponent = createAction(
|
||||
`${CANVAS_PREFIX} Navigate To Component`,
|
||||
props<{ request: NavigateToComponentRequest }>()
|
||||
|
|
|
@ -636,7 +636,12 @@ export class FlowEffects {
|
|||
map((action) => action.request),
|
||||
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
|
||||
tap(([request, processGroupId]) => {
|
||||
this.router.navigate(['/process-groups', processGroupId, request.type, request.id, 'edit']);
|
||||
const url = ['/process-groups', processGroupId, request.type, request.id, 'edit'];
|
||||
if (this.canvasView.isSelectedComponentOnScreen()) {
|
||||
this.store.dispatch(FlowActions.navigateWithoutTransform({ url }));
|
||||
} else {
|
||||
this.router.navigate(url);
|
||||
}
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
|
@ -1771,15 +1776,15 @@ export class FlowEffects {
|
|||
{ dispatch: false }
|
||||
);
|
||||
|
||||
centerSelectedComponent$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowActions.centerSelectedComponent),
|
||||
tap(() => {
|
||||
this.canvasView.centerSelectedComponent();
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
centerSelectedComponents$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowActions.centerSelectedComponents),
|
||||
map((action) => action.request),
|
||||
tap((request) => {
|
||||
this.canvasView.centerSelectedComponents(request.allowTransition);
|
||||
}),
|
||||
switchMap(() => of(FlowActions.setAllowTransition({ allowTransition: false })))
|
||||
)
|
||||
);
|
||||
|
||||
navigateToProvenanceForComponent$ = createEffect(
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
resetFlowState,
|
||||
runOnce,
|
||||
runOnceSuccess,
|
||||
setAllowTransition,
|
||||
setDragging,
|
||||
setNavigationCollapsed,
|
||||
setOperationCollapsed,
|
||||
|
@ -137,6 +138,7 @@ export const initialState: FlowState = {
|
|||
saving: false,
|
||||
transitionRequired: false,
|
||||
skipTransform: false,
|
||||
allowTransition: false,
|
||||
navigationCollapsed: false,
|
||||
operationCollapsed: false,
|
||||
error: null,
|
||||
|
@ -297,15 +299,19 @@ export const flowReducer = createReducer(
|
|||
}),
|
||||
on(setDragging, (state, { dragging }) => ({
|
||||
...state,
|
||||
dragging: dragging
|
||||
dragging
|
||||
})),
|
||||
on(setTransitionRequired, (state, { transitionRequired }) => ({
|
||||
...state,
|
||||
transitionRequired: transitionRequired
|
||||
transitionRequired
|
||||
})),
|
||||
on(setSkipTransform, (state, { skipTransform }) => ({
|
||||
...state,
|
||||
skipTransform: skipTransform
|
||||
skipTransform
|
||||
})),
|
||||
on(setAllowTransition, (state, { allowTransition }) => ({
|
||||
...state,
|
||||
allowTransition
|
||||
})),
|
||||
on(navigateWithoutTransform, (state) => ({
|
||||
...state,
|
||||
|
@ -313,11 +319,11 @@ export const flowReducer = createReducer(
|
|||
})),
|
||||
on(setNavigationCollapsed, (state, { navigationCollapsed }) => ({
|
||||
...state,
|
||||
navigationCollapsed: navigationCollapsed
|
||||
navigationCollapsed
|
||||
})),
|
||||
on(setOperationCollapsed, (state, { operationCollapsed }) => ({
|
||||
...state,
|
||||
operationCollapsed: operationCollapsed
|
||||
operationCollapsed
|
||||
})),
|
||||
on(startComponentSuccess, stopComponentSuccess, (state, { response }) => {
|
||||
return produce(state, (draftState) => {
|
||||
|
|
|
@ -80,7 +80,7 @@ export const selectAnySelectedComponentIds = createSelector(selectCurrentRoute,
|
|||
|
||||
export const selectBulkSelectedComponentIds = createSelector(selectCurrentRoute, (route) => {
|
||||
const ids: string[] = [];
|
||||
// only handle either bulk component route
|
||||
// only handle bulk component route
|
||||
if (route?.params.ids) {
|
||||
ids.push(...route.params.ids.split(','));
|
||||
}
|
||||
|
@ -140,6 +140,8 @@ export const selectDragging = createSelector(selectFlowState, (state: FlowState)
|
|||
|
||||
export const selectSkipTransform = createSelector(selectFlowState, (state: FlowState) => state.skipTransform);
|
||||
|
||||
export const selectAllowTransition = createSelector(selectFlowState, (state: FlowState) => state.allowTransition);
|
||||
|
||||
export const selectFunnels = createSelector(
|
||||
selectFlowState,
|
||||
(state: FlowState) => state.flow.processGroupFlow?.flow.funnels
|
||||
|
|
|
@ -40,6 +40,10 @@ export interface SelectComponentsRequest {
|
|||
components: SelectedComponent[];
|
||||
}
|
||||
|
||||
export interface CenterComponentRequest {
|
||||
allowTransition: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
Load Process Group
|
||||
*/
|
||||
|
@ -473,6 +477,7 @@ export interface FlowState {
|
|||
dragging: boolean;
|
||||
transitionRequired: boolean;
|
||||
skipTransform: boolean;
|
||||
allowTransition: boolean;
|
||||
saving: boolean;
|
||||
navigationCollapsed: boolean;
|
||||
operationCollapsed: boolean;
|
||||
|
|
|
@ -20,7 +20,7 @@ import { CanvasState } from '../../state';
|
|||
import { Position } from '../../state/shared';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
centerSelectedComponent,
|
||||
centerSelectedComponents,
|
||||
deselectAllComponents,
|
||||
editComponent,
|
||||
editCurrentProcessGroup,
|
||||
|
@ -38,6 +38,7 @@ import { selectTransform } from '../../state/transform/transform.selectors';
|
|||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { SelectedComponent } from '../../state/flow';
|
||||
import {
|
||||
selectAllowTransition,
|
||||
selectBulkSelectedComponentIds,
|
||||
selectConnection,
|
||||
selectCurrentProcessGroupId,
|
||||
|
@ -57,13 +58,15 @@ import {
|
|||
selectViewStatusHistoryComponent
|
||||
} from '../../state/flow/flow.selectors';
|
||||
import { filter, map, switchMap, take } from 'rxjs';
|
||||
import { restoreViewport, zoomFit } from '../../state/transform/transform.actions';
|
||||
import { restoreViewport } from '../../state/transform/transform.actions';
|
||||
import { ComponentType, isDefinedAndNotNull } from '../../../../state/shared';
|
||||
import { initialState } from '../../state/flow/flow.reducer';
|
||||
import { CanvasContextMenu } from '../../service/canvas-context-menu.service';
|
||||
import { getStatusHistoryAndOpenDialog } from '../../../../state/status-history/status-history.actions';
|
||||
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
|
||||
import { concatLatestFrom } from '@ngrx/effects';
|
||||
import { selectUrl } from '../../../../state/router/router.selectors';
|
||||
import { Storage } from '../../../../service/storage.service';
|
||||
|
||||
@Component({
|
||||
selector: 'fd-canvas',
|
||||
|
@ -81,6 +84,7 @@ export class Canvas implements OnInit, OnDestroy {
|
|||
private viewContainerRef: ViewContainerRef,
|
||||
private store: Store<CanvasState>,
|
||||
private canvasView: CanvasView,
|
||||
private storage: Storage,
|
||||
public canvasContextMenu: CanvasContextMenu
|
||||
) {
|
||||
this.store
|
||||
|
@ -90,6 +94,13 @@ export class Canvas implements OnInit, OnDestroy {
|
|||
this.scale = transform.scale;
|
||||
});
|
||||
|
||||
this.store
|
||||
.select(selectUrl)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((route) => {
|
||||
this.storage.setItem('current-canvas-route', route);
|
||||
});
|
||||
|
||||
// load the process group from the route
|
||||
this.store
|
||||
.select(selectProcessGroupIdFromRoute)
|
||||
|
@ -133,14 +144,17 @@ export class Canvas implements OnInit, OnDestroy {
|
|||
filter((processGroupId) => processGroupId != initialState.id),
|
||||
switchMap(() => this.store.select(selectSingleSelectedComponent)),
|
||||
filter((selectedComponent) => selectedComponent != null),
|
||||
concatLatestFrom(() => this.store.select(selectSkipTransform)),
|
||||
concatLatestFrom(() => [
|
||||
this.store.select(selectSkipTransform),
|
||||
this.store.select(selectAllowTransition)
|
||||
]),
|
||||
takeUntilDestroyed()
|
||||
)
|
||||
.subscribe(([, skipTransform]) => {
|
||||
.subscribe(([, skipTransform, allowTransition]) => {
|
||||
if (skipTransform) {
|
||||
this.store.dispatch(setSkipTransform({ skipTransform: false }));
|
||||
} else {
|
||||
this.store.dispatch(centerSelectedComponent());
|
||||
this.store.dispatch(centerSelectedComponents({ request: { allowTransition } }));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -151,14 +165,17 @@ export class Canvas implements OnInit, OnDestroy {
|
|||
filter((processGroupId) => processGroupId != initialState.id),
|
||||
switchMap(() => this.store.select(selectBulkSelectedComponentIds)),
|
||||
filter((ids) => ids.length > 0),
|
||||
concatLatestFrom(() => this.store.select(selectSkipTransform)),
|
||||
concatLatestFrom(() => [
|
||||
this.store.select(selectSkipTransform),
|
||||
this.store.select(selectAllowTransition)
|
||||
]),
|
||||
takeUntilDestroyed()
|
||||
)
|
||||
.subscribe(([, skipTransform]) => {
|
||||
.subscribe(([, skipTransform, allowTransition]) => {
|
||||
if (skipTransform) {
|
||||
this.store.dispatch(setSkipTransform({ skipTransform: false }));
|
||||
} else {
|
||||
this.store.dispatch(zoomFit());
|
||||
this.store.dispatch(centerSelectedComponents({ request: { allowTransition } }));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -18,27 +18,41 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FlowStatus } from './flow-status.component';
|
||||
import { Search } from '../search/search.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Component } from '@angular/core';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../../state/flow/flow.reducer';
|
||||
|
||||
describe('FlowStatus', () => {
|
||||
let component: FlowStatus;
|
||||
let fixture: ComponentFixture<FlowStatus>;
|
||||
|
||||
@Component({
|
||||
selector: 'search',
|
||||
standalone: true,
|
||||
template: ''
|
||||
})
|
||||
class MockSearch {}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FlowStatus,
|
||||
Search,
|
||||
MockSearch,
|
||||
HttpClientTestingModule,
|
||||
CdkOverlayOrigin,
|
||||
CdkConnectedOverlay,
|
||||
MatAutocompleteModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
providers: [
|
||||
provideMockStore({
|
||||
initialState
|
||||
})
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(FlowStatus);
|
||||
|
|
|
@ -186,10 +186,21 @@
|
|||
</li>
|
||||
<!-- TODO - Consider showing more context of match like existing UI -->
|
||||
<li *ngFor="let result of results" class="ml-2 py-1">
|
||||
<a *ngIf="!result.parentGroup; else resultLink" [routerLink]="['/process-groups', result.id]">
|
||||
<a *ngIf="!result.parentGroup; else componentLink" [routerLink]="['/process-groups', result.id]">
|
||||
{{ result.name }}
|
||||
</a>
|
||||
<ng-template #resultLink>
|
||||
<ng-template #componentLink>
|
||||
<a
|
||||
*ngIf="
|
||||
result.parentGroup.id == currentProcessGroupId;
|
||||
else componentInDifferentProcessGroupLink
|
||||
"
|
||||
(click)="componentLinkClicked(path, result.id)"
|
||||
[routerLink]="['/process-groups', result.parentGroup.id, path, result.id]">
|
||||
{{ result.name ? result.name : result.id }}
|
||||
</a>
|
||||
</ng-template>
|
||||
<ng-template #componentInDifferentProcessGroupLink>
|
||||
<a [routerLink]="['/process-groups', result.parentGroup.id, path, result.id]">
|
||||
{{ result.name ? result.name : result.id }}
|
||||
</a>
|
||||
|
|
|
@ -22,6 +22,8 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
|
|||
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../../state/flow/flow.reducer';
|
||||
|
||||
describe('Search', () => {
|
||||
let component: Search;
|
||||
|
@ -37,6 +39,11 @@ describe('Search', () => {
|
|||
ReactiveFormsModule,
|
||||
CdkConnectedOverlay,
|
||||
MatAutocompleteModule
|
||||
],
|
||||
providers: [
|
||||
provideMockStore({
|
||||
initialState
|
||||
})
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(Search);
|
||||
|
|
|
@ -32,6 +32,11 @@ import { NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
|
|||
import { RouterLink } from '@angular/router';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { CanvasState } from '../../../../state';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { centerSelectedComponents, setAllowTransition } from '../../../../state/flow/flow.actions';
|
||||
import { selectCurrentRoute } from '../../../../../../state/router/router.selectors';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'search',
|
||||
|
@ -86,11 +91,25 @@ export class Search implements OnInit {
|
|||
parameterProviderNodeResults: ComponentSearchResult[] = [];
|
||||
parameterResults: ComponentSearchResult[] = [];
|
||||
|
||||
selectedComponentType: ComponentType | null = null;
|
||||
selectedComponentId: string | null = null;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private searchService: SearchService
|
||||
private searchService: SearchService,
|
||||
private store: Store<CanvasState>
|
||||
) {
|
||||
this.searchForm = this.formBuilder.group({ searchBar: '' });
|
||||
|
||||
this.store
|
||||
.select(selectCurrentRoute)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((route) => {
|
||||
if (route?.params) {
|
||||
this.selectedComponentId = route.params.id;
|
||||
this.selectedComponentType = route.params.type;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -169,4 +188,12 @@ export class Search implements OnInit {
|
|||
this.parameterProviderNodeResults = [];
|
||||
this.parameterResults = [];
|
||||
}
|
||||
|
||||
componentLinkClicked(componentType: ComponentType, id: string): void {
|
||||
if (componentType == this.selectedComponentType && id == this.selectedComponentId) {
|
||||
this.store.dispatch(centerSelectedComponents({ request: { allowTransition: true } }));
|
||||
} else {
|
||||
this.store.dispatch(setAllowTransition({ allowTransition: true }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<i class="fa fa-navicon"></i>
|
||||
</button>
|
||||
<mat-menu #globalMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item class="global-menu-item" [routerLink]="['/']">
|
||||
<button mat-menu-item class="global-menu-item" [routerLink]="getCanvasLink()">
|
||||
<i class="icon icon-drop mr-2"></i>
|
||||
Canvas
|
||||
</button>
|
||||
|
|
|
@ -32,6 +32,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||
import { NiFiState } from '../../../state';
|
||||
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { Storage } from '../../../service/storage.service';
|
||||
|
||||
@Component({
|
||||
selector: 'navigation',
|
||||
|
@ -59,6 +60,7 @@ export class Navigation {
|
|||
private store: Store<NiFiState>,
|
||||
private authStorage: AuthStorage,
|
||||
private authService: AuthService,
|
||||
private storage: Storage,
|
||||
@Inject(DOCUMENT) private _document: Document
|
||||
) {}
|
||||
|
||||
|
@ -94,6 +96,11 @@ export class Navigation {
|
|||
);
|
||||
}
|
||||
|
||||
getCanvasLink(): string {
|
||||
const canvasRoute = this.storage.getItem('current-canvas-route');
|
||||
return canvasRoute || '/';
|
||||
}
|
||||
|
||||
toggleTheme(value = !this.isDarkMode) {
|
||||
this.isDarkMode = value;
|
||||
this._document.body.classList.toggle('dark-theme', value);
|
||||
|
|
Loading…
Reference in New Issue