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:
Matt Gilman 2024-02-06 15:04:35 -05:00 committed by GitHub
parent 91f339bf0f
commit 13c70c0f30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 344 additions and 102 deletions

View File

@ -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 } }));
}
},
{

View File

@ -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.
*

View File

@ -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;
}
/**

View File

@ -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;

View File

@ -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 }>()

View File

@ -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(

View File

@ -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) => {

View File

@ -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

View File

@ -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;

View File

@ -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 } }));
}
});

View File

@ -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);

View File

@ -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>

View File

@ -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);

View File

@ -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 }));
}
}
}

View File

@ -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>

View File

@ -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);