NIFI-12967: Introducing Back action (#8859)

* NIFI-12967:
- Adding support for navigating back to the users previous location in certain circumstances like going to a Controller Service, managing access policies, going to parameters, viewing documentation, opening advanced UIs, and viewing data provenance.
- Cleaning unnecessary instances of postUpdateNavigation.

* NIFI-12967:
- Updating the implementation to leverage router events and router state to simplify needed changes.

* NIFI-12967:
- Conditionally resetting or popping back navigation history because on routing context.

* NIFI-12967:
- Adding support for navigating back from queue listing and provenance.

* NIFI-12967:
- Conditionally applying the back navigation following a component update.

* NIFI-12967:
- Adding back from CS service listing.

* NIFI-12967:
- Adding back from Go To CS.
- Restoring some space in the context menu.

* NIFI-12967:
- Prevent duplicate entries in the back navigation stack.

* NIFI-12967:
- Not executing pop through navigation extras because it can result in multiple pops if the user uses their back/forward browser buttons. Instead, always popping until we encounter a back navigation that is still within a route boundary.
This commit is contained in:
Matt Gilman 2024-05-28 15:57:16 -04:00 committed by GitHub
parent 68630b64c1
commit f0e1cdcb22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 1382 additions and 443 deletions

View File

@ -19,12 +19,22 @@ import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { provideMockStore } from '@ngrx/store/testing';
import { navigationFeatureKey } from './state/navigation';
import * as fromNavigation from './state/navigation/navigation.reducer';
describe('AppComponent', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [RouterTestingModule, MatProgressSpinnerModule],
declarations: [AppComponent]
declarations: [AppComponent],
providers: [
provideMockStore({
initialState: {
[navigationFeatureKey]: fromNavigation.initialState
}
})
]
})
);

View File

@ -16,11 +16,18 @@
*/
import { Component } from '@angular/core';
import { GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationStart, Router } from '@angular/router';
import { GuardsCheckEnd, GuardsCheckStart, NavigationCancel, NavigationStart, NavigationEnd, Router } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Storage } from './service/storage.service';
import { ThemingService } from './service/theming.service';
import { MatDialog } from '@angular/material/dialog';
import { NiFiState } from './state';
import { Store } from '@ngrx/store';
import { BackNavigation } from './state/navigation';
import { popBackNavigation, pushBackNavigation } from './state/navigation/navigation.actions';
import { filter, map, tap } from 'rxjs';
import { concatLatestFrom } from '@ngrx/operators';
import { selectBackNavigation } from './state/navigation/navigation.selectors';
@Component({
selector: 'nifi',
@ -35,19 +42,44 @@ export class AppComponent {
private router: Router,
private storage: Storage,
private themingService: ThemingService,
private dialog: MatDialog
private dialog: MatDialog,
private store: Store<NiFiState>
) {
this.router.events.pipe(takeUntilDestroyed()).subscribe((event) => {
if (event instanceof NavigationStart) {
this.dialog.openDialogs.forEach((dialog) => dialog.close('ROUTED'));
}
if (event instanceof GuardsCheckStart) {
this.guardLoading = true;
}
if (event instanceof GuardsCheckEnd || event instanceof NavigationCancel) {
this.guardLoading = false;
}
});
this.router.events
.pipe(
takeUntilDestroyed(),
tap((event) => {
if (event instanceof NavigationStart) {
this.dialog.openDialogs.forEach((dialog) => dialog.close('ROUTED'));
}
if (event instanceof GuardsCheckStart) {
this.guardLoading = true;
}
if (event instanceof GuardsCheckEnd || event instanceof NavigationCancel) {
this.guardLoading = false;
}
}),
filter((e) => e instanceof NavigationEnd),
map((e) => e as NavigationEnd),
concatLatestFrom(() => this.store.select(selectBackNavigation))
)
.subscribe(([event, previousBackNavigation]) => {
const extras = this.router.getCurrentNavigation()?.extras;
if (extras?.state?.['backNavigation']) {
const backNavigation: BackNavigation = extras?.state?.['backNavigation'];
this.store.dispatch(
pushBackNavigation({
backNavigation
})
);
} else if (previousBackNavigation) {
this.store.dispatch(
popBackNavigation({
url: event.url
})
);
}
});
let theme = this.storage.getItem('theme');

View File

@ -331,6 +331,7 @@ export class ComponentAccessPolicies implements OnInit, OnDestroy {
return 'icon-group-remote';
case 'parameter-providers':
case 'parameter-contexts':
case 'reporting-tasks':
return 'icon-drop';
}
@ -353,6 +354,8 @@ export class ComponentAccessPolicies implements OnInit, OnDestroy {
return ComponentType.RemoteProcessGroup;
case 'parameter-contexts':
return 'Parameter Context';
case 'reporting-tasks':
return 'Reporting Task';
case 'parameter-providers':
return ComponentType.ParameterProvider;
}

View File

@ -229,7 +229,8 @@ export class CanvasActionsService {
navigateToManageComponentPolicies({
request: {
resource: 'process-groups',
id: extraArgs.processGroupId
id: extraArgs.processGroupId,
backNavigationContext: 'Process Group'
}
})
);
@ -239,24 +240,31 @@ export class CanvasActionsService {
const componentType: ComponentType = selectionData.type;
let resource = 'process-groups';
let backNavigationContext = 'Process Group';
switch (componentType) {
case ComponentType.Processor:
resource = 'processors';
backNavigationContext = 'Processor';
break;
case ComponentType.InputPort:
resource = 'input-ports';
backNavigationContext = 'Input Port';
break;
case ComponentType.OutputPort:
resource = 'output-ports';
backNavigationContext = 'Output Port';
break;
case ComponentType.Funnel:
resource = 'funnels';
backNavigationContext = 'Funnel';
break;
case ComponentType.Label:
resource = 'labels';
backNavigationContext = 'Label';
break;
case ComponentType.RemoteProcessGroup:
resource = 'remote-process-groups';
backNavigationContext = 'Remote Process Group';
break;
}
@ -264,7 +272,8 @@ export class CanvasActionsService {
navigateToManageComponentPolicies({
request: {
resource,
id: selectionData.id
id: selectionData.id,
backNavigationContext
}
})
);

View File

@ -72,6 +72,7 @@ import { CanvasView } from './canvas-view.service';
import { CanvasActionsService } from './canvas-actions.service';
import * as FlowActions from '../state/flow/flow.actions';
import { DraggableBehavior } from './behavior/draggable-behavior.service';
import { BackNavigation } from '../../../state/navigation';
@Injectable({ providedIn: 'root' })
export class CanvasContextMenu implements ContextMenuDefinitionProvider {
@ -660,18 +661,37 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
text: 'Parameters',
action: (selection: d3.Selection<any, any, any, any>) => {
let id;
let backNavigation;
if (selection.empty()) {
id = this.canvasUtils.getParameterContextId();
backNavigation = {
route: ['/process-groups', this.canvasUtils.getProcessGroupId()],
routeBoundary: ['/parameter-contexts'],
context: 'Process Group'
} as BackNavigation;
} else {
const selectionData = selection.datum();
id = selectionData.parameterContext.id;
backNavigation = {
route: [
'/process-groups',
this.canvasUtils.getProcessGroupId(),
ComponentType.ProcessGroup,
selectionData.id
],
routeBoundary: ['/parameter-contexts'],
context: 'Process Group'
} as BackNavigation;
}
if (id) {
this.store.dispatch(
navigateToParameterContext({
request: {
id
id,
backNavigation
}
})
);
@ -941,14 +961,27 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
clazz: 'fa fa-book',
text: 'View Documentation',
action: (selection: any) => {
const processGroupId = this.canvasUtils.getProcessGroupId();
const selectionData = selection.datum();
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: selectionData.component.type,
group: selectionData.component.bundle.group,
artifact: selectionData.component.bundle.artifact,
version: selectionData.component.bundle.version
request: {
backNavigation: {
route: [
'/process-groups',
processGroupId,
ComponentType.Processor,
selectionData.id
],
routeBoundary: ['/documentation'],
context: 'Processor'
} as BackNavigation,
parameters: {
select: selectionData.component.type,
group: selectionData.component.bundle.group,
artifact: selectionData.component.bundle.artifact,
version: selectionData.component.bundle.version
}
}
})
);

View File

@ -68,6 +68,11 @@ export const inlineCreateControllerServiceSuccess = createAction(
props<{ response: CreateControllerServiceSuccess }>()
);
export const navigateToService = createAction(
'[Controller Services] Navigate To Service',
props<{ request: SelectControllerServiceRequest }>()
);
export const navigateToEditService = createAction(
'[Controller Services] Navigate To Edit Service',
props<{ id: string }>()
@ -78,6 +83,11 @@ export const navigateToAdvancedServiceUi = createAction(
props<{ id: string }>()
);
export const navigateToManageComponentPolicies = createAction(
'[Controller Services] Navigate To Manage Component Policies',
props<{ id: string }>()
);
export const openConfigureControllerServiceDialog = createAction(
'[Controller Services] Open Configure Controller Service Dialog',
props<{ request: EditControllerServiceDialogRequest }>()

View File

@ -63,6 +63,7 @@ import {
selectPropertyVerificationStatus
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
@Injectable()
export class ControllerServicesEffects {
@ -204,7 +205,66 @@ export class ControllerServicesEffects {
map((action) => action.id),
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([id, processGroupId]) => {
this.router.navigate(['/process-groups', processGroupId, 'controller-services', id, 'advanced']);
const routeBoundary: string[] = [
'/process-groups',
processGroupId,
'controller-services',
id,
'advanced'
];
this.router.navigate([...routeBoundary], {
state: {
backNavigation: {
route: ['/process-groups', processGroupId, 'controller-services', id],
routeBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
})
),
{ dispatch: false }
);
navigateToManageComponentPolicies$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.navigateToManageComponentPolicies),
map((action) => action.id),
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([id, processGroupId]) => {
const routeBoundary: string[] = ['/access-policies'];
this.router.navigate([...routeBoundary, 'read', 'component', 'controller-services', id], {
state: {
backNavigation: {
route: ['/process-groups', processGroupId, 'controller-services', id],
routeBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
})
),
{ dispatch: false }
);
navigateToService$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.navigateToService),
map((action) => action.request),
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([request, processGroupId]) => {
const routeBoundary: string[] = ['/process-groups', request.processGroupId, 'controller-services'];
this.router.navigate([...routeBoundary, request.id], {
state: {
backNavigation: {
route: ['/process-groups', processGroupId, 'controller-services', request.id],
routeBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
})
),
{ dispatch: false }
@ -306,7 +366,7 @@ export class ControllerServicesEffects {
selectPropertyVerificationStatus
);
const goTo = (commands: string[], destination: string): void => {
const goTo = (commands: string[], destination: string, commandBoundary?: string[]): void => {
if (editDialogReference.componentInstance.editControllerServiceForm.dirty) {
const saveChangesDialogReference = this.dialog.open(YesNoDialog, {
...SMALL_DIALOG,
@ -317,14 +377,50 @@ export class ControllerServicesEffects {
});
saveChangesDialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
editDialogReference.componentInstance.submitForm(commands);
editDialogReference.componentInstance.submitForm(commands, commandBoundary);
});
saveChangesDialogReference.componentInstance.no.pipe(take(1)).subscribe(() => {
this.router.navigate(commands);
if (commandBoundary) {
this.router.navigate(commands, {
state: {
backNavigation: {
route: [
'/process-groups',
processGroupId,
'controller-services',
serviceId,
'edit'
],
routeBoundary: commandBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
} else {
this.router.navigate(commands);
}
});
} else {
this.router.navigate(commands);
if (commandBoundary) {
this.router.navigate(commands, {
state: {
backNavigation: {
route: [
'/process-groups',
processGroupId,
'controller-services',
serviceId,
'edit'
],
routeBoundary: commandBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
} else {
this.router.navigate(commands);
}
}
};
@ -338,8 +434,9 @@ export class ControllerServicesEffects {
if (parameterContext != null) {
editDialogReference.componentInstance.parameterContext = parameterContext;
editDialogReference.componentInstance.goToParameter = () => {
const commands: string[] = ['/parameter-contexts', parameterContext.id];
goTo(commands, 'Parameter');
const commandBoundary: string[] = ['/parameter-contexts'];
const commands: string[] = [...commandBoundary, parameterContext.id, 'edit'];
goTo(commands, 'Parameter', commandBoundary);
};
editDialogReference.componentInstance.convertToParameter =
@ -349,13 +446,13 @@ export class ControllerServicesEffects {
editDialogReference.componentInstance.goToService = (serviceId: string) => {
this.controllerServiceService.getControllerService(serviceId).subscribe({
next: (serviceEntity) => {
const commands: string[] = [
const commandBoundary: string[] = [
'/process-groups',
serviceEntity.component.parentGroupId,
'controller-services',
serviceEntity.id
'controller-services'
];
goTo(commands, 'Controller Service');
const commands: string[] = [...commandBoundary, serviceEntity.id];
goTo(commands, 'Controller Service', commandBoundary);
},
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(
@ -392,7 +489,9 @@ export class ControllerServicesEffects {
id: request.controllerService.id,
uri: request.controllerService.uri,
payload: updateControllerServiceRequest.payload,
postUpdateNavigation: updateControllerServiceRequest.postUpdateNavigation
postUpdateNavigation: updateControllerServiceRequest.postUpdateNavigation,
postUpdateNavigationBoundary:
updateControllerServiceRequest.postUpdateNavigationBoundary
}
})
);
@ -429,7 +528,8 @@ export class ControllerServicesEffects {
response: {
id: request.id,
controllerService: response,
postUpdateNavigation: request.postUpdateNavigation
postUpdateNavigation: request.postUpdateNavigation,
postUpdateNavigationBoundary: request.postUpdateNavigationBoundary
}
})
),
@ -462,9 +562,28 @@ export class ControllerServicesEffects {
this.actions$.pipe(
ofType(ControllerServicesActions.configureControllerServiceSuccess),
map((action) => action.response),
tap((response) => {
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([response, processGroupId]) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
if (response.postUpdateNavigationBoundary) {
this.router.navigate(response.postUpdateNavigation, {
state: {
backNavigation: {
route: [
'/process-groups',
processGroupId,
'controller-services',
response.id,
'edit'
],
routeBoundary: response.postUpdateNavigationBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
} else {
this.router.navigate(response.postUpdateNavigation);
}
} else {
this.dialog.closeAll();
}

View File

@ -41,12 +41,14 @@ export interface ConfigureControllerServiceRequest {
uri: string;
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface ConfigureControllerServiceSuccess {
id: string;
controllerService: ControllerServiceEntity;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface DeleteControllerServiceRequest {

View File

@ -106,6 +106,8 @@ import {
UpdateConnectionRequest,
UpdateConnectionSuccess,
UpdatePositionsRequest,
UpdateProcessorRequest,
UpdateProcessorResponse,
UploadProcessGroupRequest,
VersionControlInformationEntity
} from './index';
@ -460,12 +462,12 @@ export const updateComponentFailure = createAction(
export const updateProcessor = createAction(
`${CANVAS_PREFIX} Update Processor`,
props<{ request: UpdateComponentRequest }>()
props<{ request: UpdateProcessorRequest }>()
);
export const updateProcessorSuccess = createAction(
`${CANVAS_PREFIX} Update Processor Success`,
props<{ response: UpdateComponentResponse }>()
props<{ response: UpdateProcessorResponse }>()
);
export const updateConnection = createAction(

View File

@ -60,6 +60,7 @@ import {
UpdateComponentResponse,
UpdateConnectionSuccess,
UpdateProcessorRequest,
UpdateProcessorResponse,
VersionControlInformationEntity
} from './index';
import { Action, Store } from '@ngrx/store';
@ -145,6 +146,7 @@ import {
selectPropertyVerificationStatus
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
@Injectable()
export class FlowEffects {
@ -1008,7 +1010,16 @@ export class FlowEffects {
map((action) => action.id),
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([id, processGroupId]) => {
this.router.navigate(['/process-groups', processGroupId, ComponentType.Processor, id, 'advanced']);
const routeBoundary = ['/process-groups', processGroupId, ComponentType.Processor, id, 'advanced'];
this.router.navigate([...routeBoundary], {
state: {
backNavigation: {
route: ['/process-groups', processGroupId, ComponentType.Processor, id],
routeBoundary,
context: 'Processor'
} as BackNavigation
}
});
})
),
{ dispatch: false }
@ -1020,7 +1031,11 @@ export class FlowEffects {
ofType(FlowActions.navigateToParameterContext),
map((action) => action.request),
tap((request) => {
this.router.navigate(['/parameter-contexts', request.id]);
this.router.navigate(['/parameter-contexts', request.id], {
state: {
backNavigation: request.backNavigation
}
});
})
),
{ dispatch: false }
@ -1043,8 +1058,18 @@ export class FlowEffects {
this.actions$.pipe(
ofType(FlowActions.navigateToManageComponentPolicies),
map((action) => action.request),
tap((request) => {
this.router.navigate(['/access-policies', 'read', 'component', request.resource, request.id]);
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([request, processGroupId]) => {
const routeBoundary: string[] = ['/access-policies'];
this.router.navigate([...routeBoundary, 'read', 'component', request.resource, request.id], {
state: {
backNavigation: {
route: ['/process-groups', processGroupId, request.resource, request.id],
routeBoundary,
context: request.backNavigationContext
} as BackNavigation
}
});
})
),
{ dispatch: false }
@ -1055,8 +1080,23 @@ export class FlowEffects {
this.actions$.pipe(
ofType(FlowActions.navigateToQueueListing),
map((action) => action.request),
tap((request) => {
this.router.navigate(['/queue', request.connectionId]);
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([request, processGroupId]) => {
const routeBoundary: string[] = ['/queue', request.connectionId];
this.router.navigate([...routeBoundary], {
state: {
backNavigation: {
route: [
'/process-groups',
processGroupId,
ComponentType.Connection,
request.connectionId
],
routeBoundary,
context: 'Connection'
} as BackNavigation
}
});
})
),
{ dispatch: false }
@ -1110,8 +1150,25 @@ export class FlowEffects {
this.actions$.pipe(
ofType(FlowActions.navigateToControllerServicesForProcessGroup),
map((action) => action.request),
tap((request) => {
this.router.navigate(['/process-groups', request.id, 'controller-services']);
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([request, processGroupId]) => {
let route: string[];
if (processGroupId == request.id) {
route = ['/process-groups', processGroupId];
} else {
route = ['/process-groups', processGroupId, ComponentType.ProcessGroup, request.id];
}
const routeBoundary: string[] = ['/process-groups', request.id, 'controller-services'];
this.router.navigate([...routeBoundary], {
state: {
backNavigation: {
route,
routeBoundary,
context: 'Process Group'
} as BackNavigation
}
});
})
),
{ dispatch: false }
@ -1327,7 +1384,7 @@ export class FlowEffects {
selectPropertyVerificationStatus
);
const goTo = (commands: string[], destination: string): void => {
const goTo = (commands: string[], commandBoundary: string[], destination: string): void => {
if (editDialogReference.componentInstance.editProcessorForm.dirty) {
const saveChangesDialogReference = this.dialog.open(YesNoDialog, {
...SMALL_DIALOG,
@ -1338,22 +1395,51 @@ export class FlowEffects {
});
saveChangesDialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
editDialogReference.componentInstance.submitForm(commands);
editDialogReference.componentInstance.submitForm(commands, commandBoundary);
});
saveChangesDialogReference.componentInstance.no.pipe(take(1)).subscribe(() => {
this.router.navigate(commands);
this.router.navigate(commands, {
state: {
backNavigation: {
route: [
'/process-groups',
processGroupId,
ComponentType.Processor,
processorId,
'edit'
],
routeBoundary: commandBoundary,
context: 'Processor'
} as BackNavigation
}
});
});
} else {
this.router.navigate(commands);
this.router.navigate(commands, {
state: {
backNavigation: {
route: [
'/process-groups',
processGroupId,
ComponentType.Processor,
processorId,
'edit'
],
routeBoundary: commandBoundary,
context: 'Processor'
} as BackNavigation
}
});
}
};
if (parameterContext != null) {
editDialogReference.componentInstance.parameterContext = parameterContext;
editDialogReference.componentInstance.goToParameter = () => {
const commands: string[] = ['/parameter-contexts', parameterContext.id];
goTo(commands, 'Parameter');
const commandBoundary: string[] = ['/parameter-contexts'];
const commands: string[] = [...commandBoundary, parameterContext.id, 'edit'];
goTo(commands, commandBoundary, 'Parameter');
};
editDialogReference.componentInstance.convertToParameter =
@ -1363,13 +1449,13 @@ export class FlowEffects {
editDialogReference.componentInstance.goToService = (serviceId: string) => {
this.controllerServiceService.getControllerService(serviceId).subscribe({
next: (serviceEntity) => {
const commands: string[] = [
const commandBoundary: string[] = [
'/process-groups',
serviceEntity.component.parentGroupId,
'controller-services',
serviceEntity.id
'controller-services'
];
goTo(commands, 'Controller Service');
const commands: string[] = [...commandBoundary, serviceEntity.id];
goTo(commands, commandBoundary, 'Controller Service');
},
error: (errorResponse: HttpErrorResponse) => {
this.store.dispatch(this.snackBarOrFullScreenError(errorResponse));
@ -1390,14 +1476,7 @@ export class FlowEffects {
.subscribe((updateProcessorRequest: UpdateProcessorRequest) => {
this.store.dispatch(
FlowActions.updateProcessor({
request: {
id: processorId,
uri: request.uri,
type: request.type,
payload: updateProcessorRequest.payload,
errorStrategy: 'banner',
postUpdateNavigation: updateProcessorRequest.postUpdateNavigation
}
request: updateProcessorRequest
})
);
});
@ -1636,7 +1715,6 @@ export class FlowEffects {
requestId: request.requestId,
id: request.id,
type: request.type,
postUpdateNavigation: request.postUpdateNavigation,
response
};
return FlowActions.updateComponentSuccess({ response: updateComponentResponse });
@ -1661,12 +1739,8 @@ export class FlowEffects {
this.actions$.pipe(
ofType(FlowActions.updateComponentSuccess),
map((action) => action.response),
tap((response) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
} else {
this.dialog.closeAll();
}
tap(() => {
this.dialog.closeAll();
})
),
{ dispatch: false }
@ -1693,14 +1767,15 @@ export class FlowEffects {
mergeMap((request) =>
from(this.flowService.updateComponent(request)).pipe(
map((response) => {
const updateComponentResponse: UpdateComponentResponse = {
const updateProcessorResponse: UpdateProcessorResponse = {
requestId: request.requestId,
id: request.id,
type: request.type,
postUpdateNavigation: request.postUpdateNavigation,
postUpdateNavigationBoundary: request.postUpdateNavigationBoundary,
response
};
return FlowActions.updateProcessorSuccess({ response: updateComponentResponse });
return FlowActions.updateProcessorSuccess({ response: updateProcessorResponse });
}),
catchError((errorResponse: HttpErrorResponse) => {
const updateComponentFailure: UpdateComponentFailure = {
@ -1721,15 +1796,34 @@ export class FlowEffects {
this.actions$.pipe(
ofType(FlowActions.updateProcessorSuccess),
map((action) => action.response),
tap((response) => {
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([response, processGroupId]) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
if (response.postUpdateNavigationBoundary) {
this.router.navigate(response.postUpdateNavigation, {
state: {
backNavigation: {
route: [
'/process-groups',
processGroupId,
ComponentType.Processor,
response.id,
'edit'
],
routeBoundary: response.postUpdateNavigationBoundary,
context: 'Processor'
} as BackNavigation
}
});
} else {
this.router.navigate(response.postUpdateNavigation);
}
} else {
this.dialog.closeAll();
}
}),
filter((response) => response.postUpdateNavigation == null),
switchMap((response) => of(FlowActions.loadConnectionsForComponent({ id: response.id })))
filter(([response]) => response.postUpdateNavigation == null),
switchMap(([response]) => of(FlowActions.loadConnectionsForComponent({ id: response.id })))
)
);
@ -2466,8 +2560,18 @@ export class FlowEffects {
this.actions$.pipe(
ofType(FlowActions.navigateToProvenanceForComponent),
map((action) => action.id),
tap((componentId) => {
this.router.navigate(['/provenance'], { queryParams: { componentId } });
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([componentId, processGroupId]) => {
this.router.navigate(['/provenance'], {
queryParams: { componentId },
state: {
backNavigation: {
route: ['/process-groups', processGroupId, ComponentType.Processor, componentId],
routeBoundary: ['/provenance'],
context: 'Processor'
} as BackNavigation
}
});
})
),
{ dispatch: false }

View File

@ -32,6 +32,7 @@ import {
VersionedFlowSnapshotMetadataEntity
} from '../../../../state/shared';
import { HttpErrorResponse } from '@angular/common/http';
import { BackNavigation } from '../../../../state/navigation';
export const flowFeatureKey = 'flowState';
@ -346,6 +347,7 @@ export interface OpenComponentDialogRequest {
export interface NavigateToManageComponentPoliciesRequest {
resource: string;
id: string;
backNavigationContext: string;
}
export interface EditComponentDialogRequest {
@ -373,6 +375,7 @@ export interface NavigateToQueueListing {
export interface NavigateToParameterContext {
id: string;
backNavigation: BackNavigation;
}
export interface EditCurrentProcessGroupRequest {
@ -388,9 +391,9 @@ export interface EditConnectionDialogRequest extends EditComponentDialogRequest
};
}
export interface UpdateProcessorRequest {
payload: any;
export interface UpdateProcessorRequest extends UpdateComponentRequest {
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface UpdateComponentRequest {
@ -401,7 +404,6 @@ export interface UpdateComponentRequest {
payload: any;
errorStrategy: 'snackbar' | 'banner';
restoreOnFailure?: any;
postUpdateNavigation?: string[];
}
export interface UpdateComponentResponse {
@ -409,7 +411,11 @@ export interface UpdateComponentResponse {
id: string;
type: ComponentType;
response: any;
}
export interface UpdateProcessorResponse extends UpdateComponentResponse {
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface UpdateComponentFailure {

View File

@ -19,13 +19,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/flow/flow.reducer';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NewCanvasItem } from './new-canvas-item/new-canvas-item.component';
import { MatMenuModule } from '@angular/material/menu';
import { MatDividerModule } from '@angular/material/divider';
import { selectControllerBulletins, selectControllerStatus } from '../../../state/flow/flow.selectors';
import { ControllerStatus } from '../../../state/flow';
import { ControllerStatus, flowFeatureKey } from '../../../state/flow';
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Component } from '@angular/core';
@ -34,10 +33,13 @@ import { ClusterSummary } from '../../../../../state/cluster-summary';
import { selectClusterSummary } from '../../../../../state/cluster-summary/cluster-summary.selectors';
import { selectCurrentUser } from '../../../../../state/current-user/current-user.selectors';
import * as fromUser from '../../../../../state/current-user/current-user.reducer';
import * as fromNavigation from '../../../../../state/navigation/navigation.reducer';
import * as fromFlow from '../../../state/flow/flow.reducer';
import { selectFlowConfiguration } from '../../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../../state/flow-configuration/flow-configuration.reducer';
import { selectLoginConfiguration } from '../../../../../state/login-configuration/login-configuration.selectors';
import * as fromLoginConfiguration from '../../../../../state/login-configuration/login-configuration.reducer';
import { navigationFeatureKey } from '../../../../../state/navigation';
describe('HeaderComponent', () => {
let component: HeaderComponent;
@ -101,7 +103,10 @@ describe('HeaderComponent', () => {
],
providers: [
provideMockStore({
initialState,
initialState: {
[flowFeatureKey]: fromFlow.initialState,
[navigationFeatureKey]: fromNavigation.initialState
},
selectors: [
{
selector: selectClusterSummary,

View File

@ -27,6 +27,7 @@ import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { Observable, of } from 'rxjs';
import {
ComponentType,
InlineServiceCreationRequest,
InlineServiceCreationResponse,
ParameterContextEntity,
@ -300,7 +301,7 @@ export class EditProcessor extends TabbedDialog {
);
}
submitForm(postUpdateNavigation?: string[]) {
submitForm(postUpdateNavigation?: string[], postUpdateNavigationBoundary?: string[]) {
const relationshipConfiguration: RelationshipConfiguration =
this.editProcessorForm.get('relationshipConfiguration')?.value;
const autoTerminated: string[] = relationshipConfiguration.relationships
@ -351,7 +352,12 @@ export class EditProcessor extends TabbedDialog {
}
this.editProcessor.next({
id: this.request.entity.id,
uri: this.request.entity.uri,
type: ComponentType.Processor,
errorStrategy: 'banner',
postUpdateNavigation,
postUpdateNavigationBoundary,
payload
});
}

View File

@ -50,10 +50,12 @@
(viewControllerServiceDocumentation)="viewControllerServiceDocumentation($event)"
(configureControllerService)="configureControllerService($event)"
(openAdvancedUi)="openAdvancedUi($event)"
(manageAccessPolicies)="navigateToManageComponentPolicies($event)"
(enableControllerService)="enableControllerService($event)"
(disableControllerService)="disableControllerService($event)"
(viewStateControllerService)="viewStateControllerService($event)"
(changeControllerServiceVersion)="changeControllerServiceVersion($event)"
(goToControllerService)="navigateToControllerService($event)"
(deleteControllerService)="deleteControllerService($event)"></controller-service-table>
</div>
}

View File

@ -31,6 +31,8 @@ import {
loadControllerServices,
navigateToAdvancedServiceUi,
navigateToEditService,
navigateToManageComponentPolicies,
navigateToService,
openChangeControllerServiceVersionDialog,
openConfigureControllerServiceDialog,
openDisableControllerServiceDialog,
@ -49,6 +51,7 @@ import { NiFiState } from '../../../../state';
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
import { FlowConfiguration } from '../../../../state/flow-configuration';
import { DocumentationRequest } from '../../../../state/documentation';
@Component({
selector: 'controller-services',
@ -162,14 +165,26 @@ export class ControllerServices implements OnDestroy {
}
viewControllerServiceDocumentation(entity: ControllerServiceEntity): void {
const request: DocumentationRequest = {
parameters: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
}
};
if (entity.parentGroupId) {
request.backNavigation = {
route: ['/process-groups', entity.parentGroupId, 'controller-services', entity.id],
routeBoundary: ['/documentation'],
context: 'Controller Service'
};
}
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
}
request
})
);
}
@ -190,6 +205,27 @@ export class ControllerServices implements OnDestroy {
);
}
navigateToControllerService(entity: ControllerServiceEntity): void {
if (entity.parentGroupId) {
this.store.dispatch(
navigateToService({
request: {
id: entity.id,
processGroupId: entity.parentGroupId
}
})
);
}
}
navigateToManageComponentPolicies(entity: ControllerServiceEntity): void {
this.store.dispatch(
navigateToManageComponentPolicies({
id: entity.id
})
);
}
enableControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
openEnableControllerServiceDialog({

View File

@ -64,6 +64,11 @@ export const navigateToEditParameterContext = createAction(
props<{ id: string }>()
);
export const navigateToManageComponentPolicies = createAction(
'[Parameter Context Listing] Navigate To Manage Component Policies',
props<{ id: string }>()
);
export const getEffectiveParameterContextAndOpenDialog = createAction(
'[Parameter Context Listing] Get Effective Parameter Context Open Dialog',
props<{ request: GetEffectiveParameterContext }>()

View File

@ -59,6 +59,7 @@ import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
import { MEDIUM_DIALOG, SMALL_DIALOG, XL_DIALOG } from '../../../../index';
import { BackNavigation } from '../../../../state/navigation';
@Injectable()
export class ParameterContextListingEffects {
@ -201,6 +202,27 @@ export class ParameterContextListingEffects {
)
);
navigateToManageComponentPolicies$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterContextListingActions.navigateToManageComponentPolicies),
map((action) => action.id),
tap((id) => {
const routeBoundary: string[] = ['/access-policies'];
this.router.navigate([...routeBoundary, 'read', 'component', 'parameter-contexts', id], {
state: {
backNavigation: {
route: ['/parameter-contexts', id],
routeBoundary,
context: 'Parameter Context'
} as BackNavigation
}
});
})
),
{ dispatch: false }
);
navigateToEditService$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -35,7 +35,8 @@
[flowConfiguration]="(flowConfiguration$ | async)!"
(selectParameterContext)="selectParameterContext($event)"
(editParameterContext)="editParameterContext($event)"
(deleteParameterContext)="deleteParameterContext($event)"></parameter-context-table>
(deleteParameterContext)="deleteParameterContext($event)"
(manageAccessPolicies)="navigateToManageComponentPolicies($event)"></parameter-context-table>
</div>
<div class="flex justify-between">
<div class="refresh-container flex items-center gap-x-2">

View File

@ -28,6 +28,7 @@ import {
getEffectiveParameterContextAndOpenDialog,
loadParameterContexts,
navigateToEditParameterContext,
navigateToManageComponentPolicies,
openNewParameterContextDialog,
promptParameterContextDeletion,
selectParameterContext
@ -120,4 +121,12 @@ export class ParameterContextListing implements OnInit {
})
);
}
navigateToManageComponentPolicies(entity: ParameterContextEntity): void {
this.store.dispatch(
navigateToManageComponentPolicies({
id: entity.id
})
);
}
}

View File

@ -86,10 +86,7 @@
</button>
}
@if (canManageAccessPolicies()) {
<button
mat-menu-item
(click)="$event.stopPropagation()"
[routerLink]="getPolicyLink(item)">
<button mat-menu-item (click)="manageAccessPoliciesClicked(item)">
<i class="fa fa-key primary-color mr-2"></i>
Manage Access Policies
</button>

View File

@ -47,6 +47,7 @@ export class ParameterContextTable {
@Output() selectParameterContext: EventEmitter<ParameterContextEntity> = new EventEmitter<ParameterContextEntity>();
@Output() editParameterContext: EventEmitter<ParameterContextEntity> = new EventEmitter<ParameterContextEntity>();
@Output() deleteParameterContext: EventEmitter<ParameterContextEntity> = new EventEmitter<ParameterContextEntity>();
@Output() manageAccessPolicies: EventEmitter<ParameterContextEntity> = new EventEmitter<ParameterContextEntity>();
displayedColumns: string[] = ['name', 'provider', 'description', 'actions'];
dataSource: MatTableDataSource<ParameterContextEntity> = new MatTableDataSource<ParameterContextEntity>();
@ -99,6 +100,10 @@ export class ParameterContextTable {
return this.flowConfiguration?.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
}
manageAccessPoliciesClicked(entity: ParameterContextEntity): void {
this.manageAccessPolicies.next(entity);
}
canGoToParameterProvider(entity: ParameterContextEntity): boolean {
if (!this.canRead(entity)) {
return false;
@ -113,10 +118,6 @@ export class ParameterContextTable {
return ['/settings', 'parameter-providers', entity.component.parameterProviderConfiguration.id];
}
getPolicyLink(entity: ParameterContextEntity): string[] {
return ['/access-policies', 'read', 'component', 'parameter-contexts', entity.id];
}
select(entity: ParameterContextEntity): void {
this.selectParameterContext.next(entity);
}

View File

@ -73,6 +73,7 @@ export interface LoadConnectionLabelRequest {
}
export interface LoadConnectionLabelResponse {
connectionId: string;
connectionLabel: string;
}
@ -103,10 +104,15 @@ export interface FlowFileDialogRequest {
clusterNodeId?: string;
}
export interface SelectedConnection {
id: string;
label: string;
}
export interface QueueListingState {
activeListingRequest: ListingRequest | null;
completedListingRequest: ListingRequest;
connectionLabel: string;
selectedConnection: SelectedConnection | null;
loadedTimestamp: string;
status: 'pending' | 'loading' | 'error' | 'success';
}

View File

@ -66,6 +66,7 @@ export class QueueListingEffects {
return QueueListingActions.loadConnectionLabelSuccess({
response: {
connectionId: request.connectionId,
connectionLabel
}
});
@ -74,6 +75,7 @@ export class QueueListingEffects {
of(
QueueListingActions.loadConnectionLabelSuccess({
response: {
connectionId: request.connectionId,
connectionLabel: 'Connection'
}
})

View File

@ -48,7 +48,7 @@ export const initialState: QueueListingState = {
},
flowFileSummaries: []
},
connectionLabel: 'Connection',
selectedConnection: null,
loadedTimestamp: 'N/A',
status: 'pending'
};
@ -57,7 +57,10 @@ export const queueListingReducer = createReducer(
initialState,
on(loadConnectionLabelSuccess, (state, { response }) => ({
...state,
connectionLabel: response.connectionLabel
selectedConnection: {
id: response.connectionId,
label: response.connectionLabel
}
})),
on(submitQueueListingRequest, (state) => ({
...state,

View File

@ -37,9 +37,9 @@ export const selectCompletedListingRequest = createSelector(
export const selectStatus = createSelector(selectQueueListingState, (state: QueueListingState) => state.status);
export const selectConnectionLabel = createSelector(
export const selectSelectedConnection = createSelector(
selectQueueListingState,
(state: QueueListingState) => state.connectionLabel
(state: QueueListingState) => state.selectedConnection
);
export const selectLoadedTimestamp = createSelector(

View File

@ -16,7 +16,7 @@
-->
<div class="flowfile-table h-full flex flex-col">
<h3 class="queue-listing-header primary-color">{{ connectionLabel }}</h3>
<h3 class="queue-listing-header primary-color">{{ selectedConnection?.label }}</h3>
<error-banner></error-banner>
<div class="flex justify-between mb-2">
<div class="accent-color font-medium">
@ -151,11 +151,18 @@
View content
</button>
}
@if (currentUser.provenancePermissions.canRead) {
@if (selectedConnection && currentUser.provenancePermissions.canRead) {
<button
mat-menu-item
[routerLink]="['/provenance']"
[queryParams]="{ flowFileUuid: item.uuid }">
[queryParams]="{ flowFileUuid: item.uuid }"
[state]="{
backNavigation: {
route: ['/queue', selectedConnection.id],
routeBoundary: ['/provenance'],
context: 'Queue'
}
}">
<i class="icon icon-provenance primary-color mr-2"></i>
Provenance
</button>

View File

@ -23,7 +23,7 @@ import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validatio
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { RouterLink } from '@angular/router';
import { FlowFileSummary, ListingRequest } from '../../../state/queue-listing';
import { FlowFileSummary, ListingRequest, SelectedConnection } from '../../../state/queue-listing';
import { CurrentUser } from '../../../../../state/current-user';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { ClusterSummary } from '../../../../../state/cluster-summary';
@ -38,7 +38,7 @@ import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
styleUrls: ['./flowfile-table.component.scss']
})
export class FlowFileTable {
@Input() connectionLabel!: string;
@Input() selectedConnection: SelectedConnection | null = null;
@Input() set listingRequest(listingRequest: ListingRequest) {
if (listingRequest.flowFileSummaries) {

View File

@ -21,7 +21,7 @@
@if (listingRequest$ | async; as listingRequest) {
@if (about$ | async; as about) {
<flowfile-table
[connectionLabel]="(connectionLabel$ | async)!"
[selectedConnection]="(selectedConnection$ | async)!"
[listingRequest]="listingRequest"
[currentUser]="(currentUser$ | async)!"
[clusterSummary]="(clusterSummary$ | async)!"

View File

@ -20,7 +20,7 @@ import { Store } from '@ngrx/store';
import { distinctUntilChanged, filter } from 'rxjs';
import {
selectConnectionIdFromRoute,
selectConnectionLabel,
selectSelectedConnection,
selectCompletedListingRequest,
selectLoadedTimestamp,
selectStatus
@ -51,7 +51,7 @@ import { loadClusterSummary } from '../../../../state/cluster-summary/cluster-su
})
export class QueueListing implements OnInit, OnDestroy {
status$ = this.store.select(selectStatus);
connectionLabel$ = this.store.select(selectConnectionLabel);
selectedConnection$ = this.store.select(selectSelectedConnection);
loadedTimestamp$ = this.store.select(selectLoadedTimestamp);
listingRequest$ = this.store.select(selectCompletedListingRequest);
currentUser$ = this.store.select(selectCurrentUser);

View File

@ -30,9 +30,13 @@ import { ManagementControllerServiceService } from '../../service/management-con
import { CreateFlowAnalysisRule } from '../../ui/flow-analysis-rules/create-flow-analysis-rule/create-flow-analysis-rule.component';
import { Router } from '@angular/router';
import { selectSaving } from '../management-controller-services/management-controller-services.selectors';
import { OpenChangeComponentVersionDialogRequest, UpdateControllerServiceRequest } from '../../../../state/shared';
import { OpenChangeComponentVersionDialogRequest } from '../../../../state/shared';
import { EditFlowAnalysisRule } from '../../ui/flow-analysis-rules/edit-flow-analysis-rule/edit-flow-analysis-rule.component';
import { CreateFlowAnalysisRuleSuccess, EditFlowAnalysisRuleDialogRequest } from './index';
import {
CreateFlowAnalysisRuleSuccess,
EditFlowAnalysisRuleDialogRequest,
UpdateFlowAnalysisRuleRequest
} from './index';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
import * as ErrorActions from '../../../../state/error/error.actions';
import { ErrorHelper } from '../../../../service/error-helper.service';
@ -50,6 +54,7 @@ import {
selectPropertyVerificationStatus
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
@Injectable()
export class FlowAnalysisRulesEffects {
@ -290,7 +295,7 @@ export class FlowAnalysisRulesEffects {
selectPropertyVerificationStatus
);
const goTo = (commands: string[], destination: string): void => {
const goTo = (commands: string[], destination: string, commandBoundary: string[]): void => {
if (editDialogReference.componentInstance.editFlowAnalysisRuleForm.dirty) {
const saveChangesDialogReference = this.dialog.open(YesNoDialog, {
...SMALL_DIALOG,
@ -301,20 +306,37 @@ export class FlowAnalysisRulesEffects {
});
saveChangesDialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
editDialogReference.componentInstance.submitForm(commands);
editDialogReference.componentInstance.submitForm(commands, commandBoundary);
});
saveChangesDialogReference.componentInstance.no.pipe(take(1)).subscribe(() => {
this.router.navigate(commands);
this.router.navigate(commands, {
state: {
backNavigation: {
route: ['/settings', 'flow-analysis-rules', ruleId, 'edit'],
routeBoundary: commandBoundary,
context: 'Flow Analysis Rule'
} as BackNavigation
}
});
});
} else {
this.router.navigate(commands);
this.router.navigate(commands, {
state: {
backNavigation: {
route: ['/settings', 'flow-analysis-rules', ruleId, 'edit'],
routeBoundary: commandBoundary,
context: 'Flow Analysis Rule'
} as BackNavigation
}
});
}
};
editDialogReference.componentInstance.goToService = (serviceId: string) => {
const commands: string[] = ['/settings', 'management-controller-services', serviceId];
goTo(commands, 'Controller Service');
const commandBoundary: string[] = ['/settings', 'management-controller-services'];
const commands: string[] = [...commandBoundary, serviceId];
goTo(commands, 'Controller Service', commandBoundary);
};
editDialogReference.componentInstance.createNewService =
@ -326,14 +348,16 @@ export class FlowAnalysisRulesEffects {
editDialogReference.componentInstance.editFlowAnalysisRule
.pipe(takeUntil(editDialogReference.afterClosed()))
.subscribe((updateControllerServiceRequest: UpdateControllerServiceRequest) => {
.subscribe((updateFlowAnalysisRuleRequest: UpdateFlowAnalysisRuleRequest) => {
this.store.dispatch(
FlowAnalysisRuleActions.configureFlowAnalysisRule({
request: {
id: request.flowAnalysisRule.id,
uri: request.flowAnalysisRule.uri,
payload: updateControllerServiceRequest.payload,
postUpdateNavigation: updateControllerServiceRequest.postUpdateNavigation
payload: updateFlowAnalysisRuleRequest.payload,
postUpdateNavigation: updateFlowAnalysisRuleRequest.postUpdateNavigation,
postUpdateNavigationBoundary:
updateFlowAnalysisRuleRequest.postUpdateNavigationBoundary
}
})
);
@ -369,7 +393,8 @@ export class FlowAnalysisRulesEffects {
response: {
id: request.id,
flowAnalysisRule: response,
postUpdateNavigation: request.postUpdateNavigation
postUpdateNavigation: request.postUpdateNavigation,
postUpdateNavigationBoundary: request.postUpdateNavigationBoundary
}
})
),
@ -392,7 +417,19 @@ export class FlowAnalysisRulesEffects {
map((action) => action.response),
tap((response) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
if (response.postUpdateNavigationBoundary) {
this.router.navigate(response.postUpdateNavigation, {
state: {
backNavigation: {
route: ['/settings', 'flow-analysis-rules', response.id, 'edit'],
routeBoundary: response.postUpdateNavigationBoundary,
context: 'Flow Analysis Rule'
} as BackNavigation
}
});
} else {
this.router.navigate(response.postUpdateNavigation);
}
} else {
this.dialog.closeAll();
}
@ -423,8 +460,7 @@ export class FlowAnalysisRulesEffects {
FlowAnalysisRuleActions.enableFlowAnalysisRuleSuccess({
response: {
id: request.id,
flowAnalysisRule: response,
postUpdateNavigation: response.postUpdateNavigation
flowAnalysisRule: response
}
})
),
@ -440,20 +476,6 @@ export class FlowAnalysisRulesEffects {
)
);
enableFlowAnalysisRuleSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowAnalysisRuleActions.enableFlowAnalysisRuleSuccess),
map((action) => action.response),
tap((response) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
}
})
),
{ dispatch: false }
);
disableFlowAnalysisRule$ = createEffect(() =>
this.actions$.pipe(
ofType(FlowAnalysisRuleActions.disableFlowAnalysisRule),
@ -464,8 +486,7 @@ export class FlowAnalysisRulesEffects {
FlowAnalysisRuleActions.disableFlowAnalysisRuleSuccess({
response: {
id: request.id,
flowAnalysisRule: response,
postUpdateNavigation: response.postUpdateNavigation
flowAnalysisRule: response
}
})
),
@ -481,20 +502,6 @@ export class FlowAnalysisRulesEffects {
)
);
disableFlowAnalysisRuleSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowAnalysisRuleActions.disableFlowAnalysisRuleSuccess),
map((action) => action.response),
tap((response) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
}
})
),
{ dispatch: false }
);
openChangeFlowAnalysisRuleVersionDialog$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -50,29 +50,30 @@ export interface ConfigureFlowAnalysisRuleRequest {
uri: string;
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface ConfigureFlowAnalysisRuleSuccess {
id: string;
flowAnalysisRule: FlowAnalysisRuleEntity;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface UpdateFlowAnalysisRuleRequest {
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface EnableFlowAnalysisRuleSuccess {
id: string;
flowAnalysisRule: FlowAnalysisRuleEntity;
postUpdateNavigation?: string[];
}
export interface DisableFlowAnalysisRuleSuccess {
id: string;
flowAnalysisRule: FlowAnalysisRuleEntity;
postUpdateNavigation?: string[];
}
export interface EnableFlowAnalysisRuleRequest {

View File

@ -33,12 +33,14 @@ export interface ConfigureControllerServiceRequest {
uri: string;
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface ConfigureControllerServiceSuccess {
id: string;
controllerService: ControllerServiceEntity;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface DeleteControllerServiceRequest {

View File

@ -81,7 +81,12 @@ export const navigateToEditService = createAction(
);
export const navigateToAdvancedServiceUi = createAction(
'[Controller Services] Navigate To Advanced Service UI',
'[Management Controller Services] Navigate To Advanced Service UI',
props<{ id: string }>()
);
export const navigateToManageComponentPolicies = createAction(
'[Management Controller Services] Navigate To Manage Component Policies',
props<{ id: string }>()
);

View File

@ -56,6 +56,7 @@ import {
selectPropertyVerificationStatus
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
@Injectable()
export class ManagementControllerServicesEffects {
@ -191,7 +192,37 @@ export class ManagementControllerServicesEffects {
ofType(ManagementControllerServicesActions.navigateToAdvancedServiceUi),
map((action) => action.id),
tap((id) => {
this.router.navigate(['/settings', 'management-controller-services', id, 'advanced']);
const routeBoundary: string[] = ['/settings', 'management-controller-services', id, 'advanced'];
this.router.navigate([...routeBoundary], {
state: {
backNavigation: {
route: ['/settings', 'management-controller-services', id],
routeBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
})
),
{ dispatch: false }
);
navigateToManageComponentPolicies$ = createEffect(
() =>
this.actions$.pipe(
ofType(ManagementControllerServicesActions.navigateToManageComponentPolicies),
map((action) => action.id),
tap((id) => {
const routeBoundary: string[] = ['/access-policies'];
this.router.navigate([...routeBoundary, 'read', 'component', 'controller-services', id], {
state: {
backNavigation: {
route: ['/settings', 'management-controller-services', id],
routeBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
})
),
{ dispatch: false }
@ -263,7 +294,7 @@ export class ManagementControllerServicesEffects {
selectPropertyVerificationStatus
);
const goTo = (commands: string[], destination: string): void => {
const goTo = (commands: string[], destination: string, commandBoundary?: string[]): void => {
if (editDialogReference.componentInstance.editControllerServiceForm.dirty) {
const saveChangesDialogReference = this.dialog.open(YesNoDialog, {
...SMALL_DIALOG,
@ -274,20 +305,50 @@ export class ManagementControllerServicesEffects {
});
saveChangesDialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
editDialogReference.componentInstance.submitForm(commands);
editDialogReference.componentInstance.submitForm(commands, commandBoundary);
});
saveChangesDialogReference.componentInstance.no.pipe(take(1)).subscribe(() => {
this.router.navigate(commands);
if (commandBoundary) {
this.router.navigate(commands, {
state: {
backNavigation: {
route: [
'/settings',
'management-controller-services',
serviceId,
'edit'
],
routeBoundary: commandBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
} else {
this.router.navigate(commands);
}
});
} else {
this.router.navigate(commands);
if (commandBoundary) {
this.router.navigate(commands, {
state: {
backNavigation: {
route: ['/settings', 'management-controller-services', serviceId, 'edit'],
routeBoundary: commandBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
} else {
this.router.navigate(commands);
}
}
};
editDialogReference.componentInstance.goToService = (serviceId: string) => {
const commands: string[] = ['/settings', 'management-controller-services', serviceId];
goTo(commands, 'Controller Service');
const commandBoundary: string[] = ['/settings', 'management-controller-services'];
const commands: string[] = [...commandBoundary, serviceId];
goTo(commands, 'Controller Service', commandBoundary);
};
editDialogReference.componentInstance.goToReferencingComponent = (
@ -322,7 +383,9 @@ export class ManagementControllerServicesEffects {
id: request.controllerService.id,
uri: request.controllerService.uri,
payload: updateControllerServiceRequest.payload,
postUpdateNavigation: updateControllerServiceRequest.postUpdateNavigation
postUpdateNavigation: updateControllerServiceRequest.postUpdateNavigation,
postUpdateNavigationBoundary:
updateControllerServiceRequest.postUpdateNavigationBoundary
}
})
);
@ -358,7 +421,8 @@ export class ManagementControllerServicesEffects {
response: {
id: request.id,
controllerService: response,
postUpdateNavigation: request.postUpdateNavigation
postUpdateNavigation: request.postUpdateNavigation,
postUpdateNavigationBoundary: request.postUpdateNavigationBoundary
}
})
),
@ -401,7 +465,19 @@ export class ManagementControllerServicesEffects {
map((action) => action.response),
tap((response) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
if (response.postUpdateNavigationBoundary) {
this.router.navigate(response.postUpdateNavigation, {
state: {
backNavigation: {
route: ['/settings', 'management-controller-services', response.id, 'edit'],
routeBoundary: response.postUpdateNavigationBoundary,
context: 'Controller Service'
} as BackNavigation
}
});
} else {
this.router.navigate(response.postUpdateNavigation);
}
} else {
this.dialog.closeAll();
}

View File

@ -167,17 +167,20 @@ export interface ConfigureParameterProviderRequest {
uri: string;
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface ConfigureParameterProviderSuccess {
id: string;
parameterProvider: ParameterProviderEntity;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface UpdateParameterProviderRequest {
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface FetchParameterProviderParametersRequest {

View File

@ -92,6 +92,11 @@ export const navigateToAdvancedParameterProviderUi = createAction(
props<{ id: string }>()
);
export const navigateToManageAccessPolicies = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Navigate To Manage Access Policies`,
props<{ id: string }>()
);
export const navigateToFetchParameterProvider = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Navigate To Fetch Parameter Provider`,
props<{ id: string }>()

View File

@ -66,6 +66,7 @@ import {
selectPropertyVerificationStatus
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
@Injectable()
export class ParameterProvidersEffects {
@ -257,7 +258,37 @@ export class ParameterProvidersEffects {
ofType(ParameterProviderActions.navigateToAdvancedParameterProviderUi),
map((action) => action.id),
tap((id) => {
this.router.navigate(['settings', 'parameter-providers', id, 'advanced']);
const routeBoundary: string[] = ['/settings', 'parameter-providers', id, 'advanced'];
this.router.navigate([...routeBoundary], {
state: {
backNavigation: {
route: ['/settings', 'parameter-providers', id],
routeBoundary,
context: 'Parameter Provider'
} as BackNavigation
}
});
})
),
{ dispatch: false }
);
navigateToManageAccessPolicies$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterProviderActions.navigateToManageAccessPolicies),
map((action) => action.id),
tap((id) => {
const routeBoundary: string[] = ['/access-policies'];
this.router.navigate([...routeBoundary, 'read', 'component', 'parameter-providers', id], {
state: {
backNavigation: {
route: ['/settings', 'parameter-providers', id],
routeBoundary,
context: 'Parameter Provider'
} as BackNavigation
}
});
})
),
{ dispatch: false }
@ -333,7 +364,7 @@ export class ParameterProvidersEffects {
selectPropertyVerificationStatus
);
const goTo = (commands: string[], destination: string) => {
const goTo = (commands: string[], destination: string, commandBoundary: string[]) => {
// confirm navigating away while changes are unsaved
if (editDialogReference.componentInstance.editParameterProviderForm.dirty) {
const promptSaveDialogRef = this.dialog.open(YesNoDialog, {
@ -345,25 +376,43 @@ export class ParameterProvidersEffects {
});
promptSaveDialogRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
editDialogReference.componentInstance.submitForm(commands);
editDialogReference.componentInstance.submitForm(commands, commandBoundary);
});
promptSaveDialogRef.componentInstance.no.pipe(take(1)).subscribe(() => {
this.router.navigate(commands);
this.router.navigate(commands, {
state: {
backNavigation: {
route: ['/settings', 'parameter-providers', id, 'edit'],
routeBoundary: commandBoundary,
context: 'Parameter Provider'
} as BackNavigation
}
});
});
} else {
this.router.navigate(commands);
this.router.navigate(commands, {
state: {
backNavigation: {
route: ['/settings', 'parameter-providers', id, 'edit'],
routeBoundary: commandBoundary,
context: 'Parameter Provider'
} as BackNavigation
}
});
}
};
editDialogReference.componentInstance.goToReferencingParameterContext = (id: string) => {
const commands: string[] = ['parameter-contexts', id];
goTo(commands, 'Parameter Context');
const commandBoundary: string[] = ['/parameter-contexts'];
const commands: string[] = [...commandBoundary, id];
goTo(commands, 'Parameter Context', commandBoundary);
};
editDialogReference.componentInstance.goToService = (serviceId: string) => {
const commands: string[] = ['/settings', 'management-controller-services', serviceId];
goTo(commands, 'Controller Service');
const commandBoundary: string[] = ['/settings', 'management-controller-services'];
const commands: string[] = [...commandBoundary, serviceId];
goTo(commands, 'Controller Service', commandBoundary);
};
editDialogReference.componentInstance.createNewProperty =
@ -385,7 +434,8 @@ export class ParameterProvidersEffects {
id: request.parameterProvider.id,
uri: request.parameterProvider.uri,
payload: updateRequest.payload,
postUpdateNavigation: updateRequest.postUpdateNavigation
postUpdateNavigation: updateRequest.postUpdateNavigation,
postUpdateNavigationBoundary: updateRequest.postUpdateNavigationBoundary
}
})
);
@ -421,7 +471,8 @@ export class ParameterProvidersEffects {
response: {
id: request.id,
parameterProvider: response,
postUpdateNavigation: request.postUpdateNavigation
postUpdateNavigation: request.postUpdateNavigation,
postUpdateNavigationBoundary: request.postUpdateNavigationBoundary
}
})
),
@ -448,7 +499,19 @@ export class ParameterProvidersEffects {
map((action) => action.response),
tap((response) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
if (response.postUpdateNavigationBoundary) {
this.router.navigate(response.postUpdateNavigation, {
state: {
backNavigation: {
route: ['/settings', 'parameter-providers', response.id, 'edit'],
routeBoundary: response.postUpdateNavigationBoundary,
context: 'Parameter Provider'
} as BackNavigation
}
});
} else {
this.router.navigate(response.postUpdateNavigation);
}
} else {
this.dialog.closeAll();
}

View File

@ -51,12 +51,14 @@ export interface EditRegistryClientRequest {
uri: string;
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface EditRegistryClientRequestSuccess {
id: string;
registryClient: RegistryClientEntity;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface DeleteRegistryClientRequest {

View File

@ -37,6 +37,7 @@ import * as ErrorActions from '../../../../state/error/error.actions';
import { ErrorHelper } from '../../../../service/error-helper.service';
import { HttpErrorResponse } from '@angular/common/http';
import { LARGE_DIALOG, MEDIUM_DIALOG, SMALL_DIALOG } from '../../../../index';
import { BackNavigation } from '../../../../state/navigation';
@Injectable()
export class RegistryClientsEffects {
@ -193,7 +194,8 @@ export class RegistryClientsEffects {
this.propertyTableHelperService.createNewProperty(registryClientId, this.registryClientService);
editDialogReference.componentInstance.goToService = (serviceId: string) => {
const commands: string[] = ['/settings', 'management-controller-services', serviceId];
const commandBoundary: string[] = ['/settings', 'management-controller-services'];
const commands: string[] = [...commandBoundary, serviceId];
if (editDialogReference.componentInstance.editRegistryClientForm.dirty) {
const saveChangesDialogReference = this.dialog.open(YesNoDialog, {
@ -205,14 +207,30 @@ export class RegistryClientsEffects {
});
saveChangesDialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
editDialogReference.componentInstance.submitForm(commands);
editDialogReference.componentInstance.submitForm(commands, commandBoundary);
});
saveChangesDialogReference.componentInstance.no.pipe(take(1)).subscribe(() => {
this.router.navigate(commands);
this.router.navigate(commands, {
state: {
backNavigation: {
route: ['/settings', 'registry-clients', registryClientId, 'edit'],
routeBoundary: commandBoundary,
context: 'Registry Client'
} as BackNavigation
}
});
});
} else {
this.router.navigate(commands);
this.router.navigate(commands, {
state: {
backNavigation: {
route: ['/settings', 'registry-clients', registryClientId, 'edit'],
routeBoundary: commandBoundary,
context: 'Registry Client'
} as BackNavigation
}
});
}
};
@ -262,7 +280,8 @@ export class RegistryClientsEffects {
response: {
id: request.id,
registryClient: response,
postUpdateNavigation: request.postUpdateNavigation
postUpdateNavigation: request.postUpdateNavigation,
postUpdateNavigationBoundary: request.postUpdateNavigationBoundary
}
})
),
@ -285,7 +304,19 @@ export class RegistryClientsEffects {
map((action) => action.response),
tap((response) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
if (response.postUpdateNavigationBoundary) {
this.router.navigate(response.postUpdateNavigation, {
state: {
backNavigation: {
route: ['/settings', 'registry-clients', response.id, 'edit'],
routeBoundary: response.postUpdateNavigationBoundary,
context: 'Registry Client'
} as BackNavigation
}
});
} else {
this.router.navigate(response.postUpdateNavigation);
}
} else {
this.dialog.closeAll();
}

View File

@ -50,24 +50,20 @@ export interface ConfigureReportingTaskRequest {
uri: string;
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface ConfigureReportingTaskSuccess {
id: string;
reportingTask: ReportingTaskEntity;
postUpdateNavigation?: string[];
}
export interface ConfigureReportingTaskRequest {
id: string;
uri: string;
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface UpdateReportingTaskRequest {
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface EditReportingTaskDialogRequest {

View File

@ -89,6 +89,11 @@ export const navigateToAdvancedReportingTaskUi = createAction(
props<{ id: string }>()
);
export const navigateToManageAccessPolicies = createAction(
'[Reporting Tasks] Navigate To Manage Access Policies',
props<{ id: string }>()
);
export const startReportingTask = createAction(
'[Reporting Tasks] Start Reporting Task',
props<{ request: StartReportingTaskRequest }>()

View File

@ -29,9 +29,9 @@ import { ReportingTaskService } from '../../service/reporting-task.service';
import { CreateReportingTask } from '../../ui/reporting-tasks/create-reporting-task/create-reporting-task.component';
import { Router } from '@angular/router';
import { selectSaving } from '../management-controller-services/management-controller-services.selectors';
import { OpenChangeComponentVersionDialogRequest, UpdateControllerServiceRequest } from '../../../../state/shared';
import { OpenChangeComponentVersionDialogRequest } from '../../../../state/shared';
import { EditReportingTask } from '../../ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component';
import { CreateReportingTaskSuccess, EditReportingTaskDialogRequest } from './index';
import { CreateReportingTaskSuccess, EditReportingTaskDialogRequest, UpdateReportingTaskRequest } from './index';
import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
import * as ErrorActions from '../../../../state/error/error.actions';
@ -50,6 +50,7 @@ import {
selectPropertyVerificationStatus
} from '../../../../state/property-verification/property-verification.selectors';
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
import { BackNavigation } from '../../../../state/navigation';
@Injectable()
export class ReportingTasksEffects {
@ -234,7 +235,37 @@ export class ReportingTasksEffects {
ofType(ReportingTaskActions.navigateToAdvancedReportingTaskUi),
map((action) => action.id),
tap((id) => {
this.router.navigate(['/settings', 'reporting-tasks', id, 'advanced']);
const routeBoundary: string[] = ['/settings', 'reporting-tasks', id, 'advanced'];
this.router.navigate([...routeBoundary], {
state: {
backNavigation: {
route: ['/settings', 'reporting-tasks', id],
routeBoundary,
context: 'Reporting Task'
} as BackNavigation
}
});
})
),
{ dispatch: false }
);
navigateToManageAccessPolicies$ = createEffect(
() =>
this.actions$.pipe(
ofType(ReportingTaskActions.navigateToManageAccessPolicies),
map((action) => action.id),
tap((id) => {
const routeBoundary = ['/access-policies'];
this.router.navigate([...routeBoundary, 'read', 'component', 'reporting-tasks', id], {
state: {
backNavigation: {
route: ['/settings', 'reporting-tasks', id],
routeBoundary,
context: 'Reporting Task'
} as BackNavigation
}
});
})
),
{ dispatch: false }
@ -302,7 +333,7 @@ export class ReportingTasksEffects {
selectPropertyVerificationStatus
);
const goTo = (commands: string[], destination: string): void => {
const goTo = (commands: string[], destination: string, commandBoundary: string[]): void => {
if (editDialogReference.componentInstance.editReportingTaskForm.dirty) {
const saveChangesDialogReference = this.dialog.open(YesNoDialog, {
...SMALL_DIALOG,
@ -313,20 +344,37 @@ export class ReportingTasksEffects {
});
saveChangesDialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
editDialogReference.componentInstance.submitForm(commands);
editDialogReference.componentInstance.submitForm(commands, commandBoundary);
});
saveChangesDialogReference.componentInstance.no.pipe(take(1)).subscribe(() => {
this.router.navigate(commands);
this.router.navigate(commands, {
state: {
backNavigation: {
route: ['/settings', 'reporting-tasks', taskId, 'edit'],
routeBoundary: commandBoundary,
context: 'Reporting Task'
} as BackNavigation
}
});
});
} else {
this.router.navigate(commands);
this.router.navigate(commands, {
state: {
backNavigation: {
route: ['/settings', 'reporting-tasks', taskId, 'edit'],
routeBoundary: commandBoundary,
context: 'Reporting Task'
} as BackNavigation
}
});
}
};
editDialogReference.componentInstance.goToService = (serviceId: string) => {
const commands: string[] = ['/settings', 'management-controller-services', serviceId];
goTo(commands, 'Controller Service');
const commandBoundary: string[] = ['/settings', 'management-controller-services'];
const commands: string[] = [...commandBoundary, serviceId];
goTo(commands, 'Controller Service', commandBoundary);
};
editDialogReference.componentInstance.createNewService =
@ -338,14 +386,16 @@ export class ReportingTasksEffects {
editDialogReference.componentInstance.editReportingTask
.pipe(takeUntil(editDialogReference.afterClosed()))
.subscribe((updateControllerServiceRequest: UpdateControllerServiceRequest) => {
.subscribe((updateReportingTaskRequest: UpdateReportingTaskRequest) => {
this.store.dispatch(
ReportingTaskActions.configureReportingTask({
request: {
id: request.reportingTask.id,
uri: request.reportingTask.uri,
payload: updateControllerServiceRequest.payload,
postUpdateNavigation: updateControllerServiceRequest.postUpdateNavigation
payload: updateReportingTaskRequest.payload,
postUpdateNavigation: updateReportingTaskRequest.postUpdateNavigation,
postUpdateNavigationBoundary:
updateReportingTaskRequest.postUpdateNavigationBoundary
}
})
);
@ -381,7 +431,8 @@ export class ReportingTasksEffects {
response: {
id: request.id,
reportingTask: response,
postUpdateNavigation: request.postUpdateNavigation
postUpdateNavigation: request.postUpdateNavigation,
postUpdateNavigationBoundary: request.postUpdateNavigationBoundary
}
})
),
@ -404,7 +455,19 @@ export class ReportingTasksEffects {
map((action) => action.response),
tap((response) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
if (response.postUpdateNavigationBoundary) {
this.router.navigate(response.postUpdateNavigation, {
state: {
backNavigation: {
route: ['/settings', 'reporting-tasks', response.id, 'edit'],
routeBoundary: response.postUpdateNavigationBoundary,
context: 'Reporting Task'
} as BackNavigation
}
});
} else {
this.router.navigate(response.postUpdateNavigation);
}
} else {
this.dialog.closeAll();
}

View File

@ -140,7 +140,7 @@ export class EditFlowAnalysisRule extends TabbedDialog {
return this.nifiCommon.formatBundle(entity.component.bundle);
}
submitForm(postUpdateNavigation?: string[]) {
submitForm(postUpdateNavigation?: string[], postUpdateNavigationBoundary?: string[]) {
const payload: any = {
revision: this.client.getRevision(this.request.flowAnalysisRule),
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
@ -164,7 +164,8 @@ export class EditFlowAnalysisRule extends TabbedDialog {
this.editFlowAnalysisRule.next({
payload,
postUpdateNavigation
postUpdateNavigation,
postUpdateNavigationBoundary
});
}

View File

@ -111,11 +111,18 @@ export class FlowAnalysisRules implements OnInit, OnDestroy {
viewFlowAnalysisRuleDocumentation(entity: FlowAnalysisRuleEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
request: {
backNavigation: {
route: ['/settings', 'flow-analysis-rules', entity.id],
routeBoundary: ['/documentation'],
context: 'Flow Analysis Rule'
},
parameters: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
}
}
})
);

View File

@ -42,6 +42,7 @@
(selectControllerService)="selectControllerService($event)"
(viewControllerServiceDocumentation)="viewControllerServiceDocumentation($event)"
(configureControllerService)="configureControllerService($event)"
(manageAccessPolicies)="navigateToManageComponentPolicies($event)"
(openAdvancedUi)="openAdvancedUi($event)"
(enableControllerService)="enableControllerService($event)"
(disableControllerService)="disableControllerService($event)"

View File

@ -28,6 +28,7 @@ import {
loadManagementControllerServices,
navigateToAdvancedServiceUi,
navigateToEditService,
navigateToManageComponentPolicies,
openChangeMgtControllerServiceVersionDialog,
openConfigureControllerServiceDialog,
openDisableControllerServiceDialog,
@ -114,11 +115,18 @@ export class ManagementControllerServices implements OnInit, OnDestroy {
viewControllerServiceDocumentation(entity: ControllerServiceEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
request: {
backNavigation: {
route: ['/settings', 'management-controller-services', entity.id],
routeBoundary: ['/documentation'],
context: 'Controller Service'
},
parameters: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
}
}
})
);
@ -132,6 +140,14 @@ export class ManagementControllerServices implements OnInit, OnDestroy {
);
}
navigateToManageComponentPolicies(entity: ControllerServiceEntity): void {
this.store.dispatch(
navigateToManageComponentPolicies({
id: entity.id
})
);
}
openAdvancedUi(entity: ControllerServiceEntity): void {
this.store.dispatch(
navigateToAdvancedServiceUi({

View File

@ -129,7 +129,7 @@ export class EditParameterProvider extends TabbedDialog {
return this.nifiCommon.formatBundle(entity.component.bundle);
}
submitForm(postUpdateNavigation?: string[]) {
submitForm(postUpdateNavigation?: string[], postUpdateNavigationBoundary?: string[]) {
const payload: any = {
revision: this.client.getRevision(this.request.parameterProvider),
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
@ -151,7 +151,8 @@ export class EditParameterProvider extends TabbedDialog {
this.editParameterProvider.next({
payload,
postUpdateNavigation
postUpdateNavigation,
postUpdateNavigationBoundary
});
}

View File

@ -134,10 +134,7 @@
</button>
}
@if (canManageAccessPolicies()) {
<button
mat-menu-item
(click)="$event.stopPropagation()"
[routerLink]="getPolicyLink(item)">
<button mat-menu-item (click)="manageAccessPoliciesClicked(item)">
<i class="fa fa-key primary-color mr-2"></i>
Manage Access Policies
</button>

View File

@ -212,7 +212,7 @@ export class ParameterProvidersTable {
this.deleteParameterProvider.next(entity);
}
getPolicyLink(entity: ParameterProviderEntity): string[] {
return ['/access-policies', 'read', 'component', 'parameter-providers', entity.id];
manageAccessPoliciesClicked(entity: ParameterProviderEntity) {
this.manageAccessPolicies.next(entity);
}
}

View File

@ -43,6 +43,7 @@
(deleteParameterProvider)="deleteParameterProvider($event)"
(configureParameterProvider)="openConfigureParameterProviderDialog($event)"
(openAdvancedUi)="openAdvancedUi($event)"
(manageAccessPolicies)="navigateToManageAccessPolicies($event)"
(fetchParameterProvider)="fetchParameterProviderParameters($event)"
(viewParameterProviderDocumentation)="viewParameterProviderDocumentation($event)"
(selectParameterProvider)="selectParameterProvider($event)"></parameter-providers-table>

View File

@ -137,14 +137,29 @@ export class ParameterProviders implements OnInit, OnDestroy {
);
}
navigateToManageAccessPolicies(parameterProvider: ParameterProviderEntity) {
this.store.dispatch(
ParameterProviderActions.navigateToManageAccessPolicies({
id: parameterProvider.id
})
);
}
viewParameterProviderDocumentation(parameterProvider: ParameterProviderEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: parameterProvider.component.type,
group: parameterProvider.component.bundle.group,
artifact: parameterProvider.component.bundle.artifact,
version: parameterProvider.component.bundle.version
request: {
backNavigation: {
route: ['/settings', 'parameter-providers', parameterProvider.id],
routeBoundary: ['/documentation'],
context: 'Parameter Provider'
},
parameters: {
select: parameterProvider.component.type,
group: parameterProvider.component.bundle.group,
artifact: parameterProvider.component.bundle.artifact,
version: parameterProvider.component.bundle.version
}
}
})
);

View File

@ -105,7 +105,7 @@ export class EditRegistryClient extends TabbedDialog {
return this.nifiCommon.formatType(entity.component);
}
submitForm(postUpdateNavigation?: string[]) {
submitForm(postUpdateNavigation?: string[], postUpdateNavigationBoundary?: string[]) {
const payload: any = {
revision: this.client.getRevision(this.request.registryClient),
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
@ -132,7 +132,8 @@ export class EditRegistryClient extends TabbedDialog {
id: this.request.registryClient.id,
uri: this.request.registryClient.uri,
payload,
postUpdateNavigation
postUpdateNavigation,
postUpdateNavigationBoundary
});
}

View File

@ -179,7 +179,7 @@ export class EditReportingTask extends TabbedDialog {
return this.nifiCommon.formatBundle(entity.component.bundle);
}
submitForm(postUpdateNavigation?: string[]) {
submitForm(postUpdateNavigation?: string[], postUpdateNavigationBoundary?: string[]) {
const payload: any = {
revision: this.client.getRevision(this.request.reportingTask),
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
@ -204,7 +204,8 @@ export class EditReportingTask extends TabbedDialog {
this.editReportingTask.next({
payload,
postUpdateNavigation
postUpdateNavigation,
postUpdateNavigationBoundary
});
}

View File

@ -160,10 +160,7 @@
</button>
}
@if (canManageAccessPolicies()) {
<button
mat-menu-item
(click)="$event.stopPropagation()"
[routerLink]="getPolicyLink(item)">
<button mat-menu-item (click)="managedAccessPoliciesClicked(item)">
<i class="fa fa-key primary-color mr-2"></i>
Manage Access Policies
</button>

View File

@ -55,6 +55,7 @@ export class ReportingTaskTable {
@Output() startReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() configureReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() openAdvancedUi: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() manageAccessPolicies: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() viewStateReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() stopReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() changeReportingTaskVersion: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@ -253,8 +254,8 @@ export class ReportingTaskTable {
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
}
getPolicyLink(entity: ReportingTaskEntity): string[] {
return ['/access-policies', 'read', 'component', 'reporting-tasks', entity.id];
managedAccessPoliciesClicked(entity: ReportingTaskEntity): void {
this.manageAccessPolicies.next(entity);
}
select(entity: ReportingTaskEntity): void {

View File

@ -38,6 +38,7 @@
[flowConfiguration]="(flowConfiguration$ | async)!"
(configureReportingTask)="configureReportingTask($event)"
(openAdvancedUi)="openAdvancedUi($event)"
(manageAccessPolicies)="navigateToManageAccessPolicies($event)"
(viewStateReportingTask)="viewStateReportingTask($event)"
(selectReportingTask)="selectReportingTask($event)"
(viewReportingTaskDocumentation)="viewReportingTaskDocumentation($event)"

View File

@ -30,6 +30,7 @@ import {
loadReportingTasks,
navigateToAdvancedReportingTaskUi,
navigateToEditReportingTask,
navigateToManageAccessPolicies,
openChangeReportingTaskVersionDialog,
openConfigureReportingTaskDialog,
openNewReportingTaskDialog,
@ -119,14 +120,29 @@ export class ReportingTasks implements OnInit, OnDestroy {
);
}
navigateToManageAccessPolicies(entity: ReportingTaskEntity): void {
this.store.dispatch(
navigateToManageAccessPolicies({
id: entity.id
})
);
}
viewReportingTaskDocumentation(entity: ReportingTaskEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
request: {
backNavigation: {
route: ['/settings', 'reporting-tasks', entity.id],
routeBoundary: ['/documentation'],
context: 'Reporting Task'
},
parameters: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
}
}
})
);

View File

@ -16,12 +16,12 @@
*/
import { createAction, props } from '@ngrx/store';
import { DocumentationParameters } from './index';
import { DocumentationRequest } from './index';
export const navigateToComponentDocumentation = createAction(
'[Documentation] Navigate To Component Documentation',
props<{
params: DocumentationParameters;
request: DocumentationRequest;
}>()
);

View File

@ -18,7 +18,7 @@
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as DocumentationActions from './documentation.actions';
import { tap } from 'rxjs';
import { map, tap } from 'rxjs';
import { Router } from '@angular/router';
@Injectable()
@ -32,8 +32,13 @@ export class DocumentationEffects {
() =>
this.actions$.pipe(
ofType(DocumentationActions.navigateToComponentDocumentation),
tap(() => {
this.router.navigate(['/documentation']);
map((action) => action.request),
tap((request) => {
this.router.navigate(['/documentation'], {
state: {
backNavigation: request.backNavigation
}
});
})
),
{ dispatch: false }

View File

@ -25,9 +25,9 @@ export const initialState: DocumentationState = {
export const documentationReducer = createReducer(
initialState,
on(navigateToComponentDocumentation, (state, { params }) => ({
on(navigateToComponentDocumentation, (state, { request }) => ({
...state,
documentationParameters: params
documentationParameters: request.parameters
})),
on(clearDocumentationParameters, (state) => ({
...state,

View File

@ -15,8 +15,15 @@
* limitations under the License.
*/
import { BackNavigation } from '../navigation';
export const documentationFeatureKey = 'documentation';
export interface DocumentationRequest {
backNavigation?: BackNavigation;
parameters: DocumentationParameters;
}
export interface DocumentationParameters {
[key: string]: string;
}

View File

@ -43,6 +43,8 @@ import { loginConfigurationFeatureKey, LoginConfigurationState } from './login-c
import { loginConfigurationReducer } from './login-configuration/login-configuration.reducer';
import { propertyVerificationFeatureKey, PropertyVerificationState } from './property-verification';
import { propertyVerificationReducer } from './property-verification/property-verification.reducer';
import { navigationFeatureKey, NavigationState } from './navigation';
import { navigationReducer } from './navigation/navigation.reducer';
export interface NiFiState {
[DEFAULT_ROUTER_FEATURENAME]: RouterReducerState;
@ -50,6 +52,7 @@ export interface NiFiState {
[currentUserFeatureKey]: CurrentUserState;
[extensionTypesFeatureKey]: ExtensionTypesState;
[aboutFeatureKey]: AboutState;
[navigationFeatureKey]: NavigationState;
[flowConfigurationFeatureKey]: FlowConfigurationState;
[loginConfigurationFeatureKey]: LoginConfigurationState;
[statusHistoryFeatureKey]: StatusHistoryState;
@ -67,6 +70,7 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
[currentUserFeatureKey]: currentUserReducer,
[extensionTypesFeatureKey]: extensionTypesReducer,
[aboutFeatureKey]: aboutReducer,
[navigationFeatureKey]: navigationReducer,
[flowConfigurationFeatureKey]: flowConfigurationReducer,
[loginConfigurationFeatureKey]: loginConfigurationReducer,
[statusHistoryFeatureKey]: statusHistoryReducer,

View File

@ -0,0 +1,28 @@
/*
* 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 navigationFeatureKey = 'navigation';
export interface BackNavigation {
route: string[];
routeBoundary: string[];
context: string;
}
export interface NavigationState {
backNavigations: BackNavigation[];
}

View File

@ -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 { createAction, props } from '@ngrx/store';
import { BackNavigation } from './index';
export const pushBackNavigation = createAction(
'[Navigation] Push Back Navigation',
props<{ backNavigation: BackNavigation }>()
);
export const popBackNavigation = createAction('[Navigation] Pop Back Navigation', props<{ url: string }>());

View File

@ -0,0 +1,70 @@
/*
* 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 { NavigationState } from './index';
import { popBackNavigation, pushBackNavigation } from './navigation.actions';
import { produce } from 'immer';
export const initialState: NavigationState = {
backNavigations: []
};
export const navigationReducer = createReducer(
initialState,
on(pushBackNavigation, (state, { backNavigation }) => {
return produce(state, (draftState) => {
if (draftState.backNavigations.length > 0) {
const currentBackNavigation = draftState.backNavigations[draftState.backNavigations.length - 1];
// don't push multiple back navigations going to the same route
if (routesNotEqual(currentBackNavigation.route, backNavigation.route)) {
draftState.backNavigations.push(backNavigation);
}
} else {
draftState.backNavigations.push(backNavigation);
}
});
}),
on(popBackNavigation, (state, { url }) => {
return produce(state, (draftState) => {
// pop any back navigation that is outside the bounds of the current url
while (draftState.backNavigations.length > 0) {
const lastBackNavigation = draftState.backNavigations[draftState.backNavigations.length - 1];
if (!url.startsWith(lastBackNavigation.routeBoundary.join('/'))) {
draftState.backNavigations.pop();
} else {
break;
}
}
});
})
);
function routesNotEqual(route1: string[], route2: string[]) {
if (route1.length !== route2.length) {
return true;
}
for (let i = 0; i < route1.length; i++) {
if (route1[i] !== route2[i]) {
return true;
}
}
return false;
}

View File

@ -0,0 +1,29 @@
/*
* 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 { navigationFeatureKey, NavigationState } from './index';
export const selectNavigationState = createFeatureSelector<NavigationState>(navigationFeatureKey);
export const selectBackNavigation = createSelector(selectNavigationState, (state: NavigationState) => {
if (state.backNavigations.length > 0) {
return state.backNavigations[state.backNavigations.length - 1];
}
return null;
});

View File

@ -135,6 +135,7 @@ export interface EditControllerServiceDialogRequest {
export interface UpdateControllerServiceRequest {
payload: any;
postUpdateNavigation?: string[];
postUpdateNavigationBoundary?: string[];
}
export interface SetEnableControllerServiceDialogRequest {

View File

@ -21,16 +21,18 @@ import { AdvancedUi } from './advanced-ui.component';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/documentation/documentation.reducer';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
import * as fromUser from '../../../state/current-user/current-user.reducer';
import * as fromNavigation from '../../../state/navigation/navigation.reducer';
import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
import * as fromClusterSummary from '../../../state/cluster-summary/cluster-summary.reducer';
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
import { selectLoginConfiguration } from '../../../state/login-configuration/login-configuration.selectors';
import * as fromLoginConfiguration from '../../../state/login-configuration/login-configuration.reducer';
import { currentUserFeatureKey } from '../../../state/current-user';
import { navigationFeatureKey } from '../../../state/navigation';
describe('AdvancedUi', () => {
let component: AdvancedUi;
@ -48,7 +50,10 @@ describe('AdvancedUi', () => {
imports: [AdvancedUi, HttpClientTestingModule, RouterTestingModule, MockNavigation],
providers: [
provideMockStore({
initialState,
initialState: {
[currentUserFeatureKey]: fromUser.initialState,
[navigationFeatureKey]: fromNavigation.initialState
},
selectors: [
{
selector: selectCurrentUser,

View File

@ -28,8 +28,10 @@
@if (hasSubMenu(item)) {
@if (menuComponent.menu; as subMenu) {
<button class="context-menu-item pl-1 pr-1 pt-2 pb-2" cdkMenuItem [cdkMenuTriggerFor]="subMenu">
<div class="context-menu-item-img" [class]="item.clazz"></div>
<div class="context-menu-item-text surface-contrast">{{ item.text }}</div>
<div class="flex gap-x-1">
<div class="context-menu-item-img" [class]="item.clazz"></div>
<div class="context-menu-item-text surface-contrast">{{ item.text }}</div>
</div>
<div class="context-menu-group-item-img fa fa-caret-right"></div>
</button>
}
@ -42,7 +44,7 @@
class="context-menu-item pl-1 pr-1 pt-2 pb-2 flex justify-between"
(click)="menuItemClicked(item, $event)"
cdkMenuItem>
<div class="flex">
<div class="flex gap-x-1">
<div class="context-menu-item-img" [class]="item.clazz"></div>
<div class="context-menu-item-text surface-contrast">{{ item.text }}</div>
</div>

View File

@ -167,16 +167,13 @@
</button>
}
@if (canManageAccessPolicies()) {
<button
mat-menu-item
(click)="$event.stopPropagation()"
[routerLink]="getPolicyLink(item)">
<button mat-menu-item (click)="manageAccessPoliciesClicked(item)">
<i class="fa fa-key primary-color mr-2"></i>
Manage Access Policies
</button>
}
} @else {
<button mat-menu-item [routerLink]="getServiceLink(item)">
<button mat-menu-item (click)="goToControllerServiceClicked(item)">
<i class="fa fa-long-arrow-right primary-color mr-2"></i>
Go To
</button>

View File

@ -77,6 +77,7 @@ export class ControllerServiceTable {
new EventEmitter<ControllerServiceEntity>();
@Output() configureControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@Output() manageAccessPolicies: EventEmitter<ControllerServiceEntity> = new EventEmitter<ControllerServiceEntity>();
@Output() openAdvancedUi: EventEmitter<ControllerServiceEntity> = new EventEmitter<ControllerServiceEntity>();
@Output() enableControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@ -86,6 +87,8 @@ export class ControllerServiceTable {
new EventEmitter<ControllerServiceEntity>();
@Output() changeControllerServiceVersion: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@Output() goToControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
protected readonly TextTip = TextTip;
protected readonly BulletinsTip = BulletinsTip;
@ -191,12 +194,8 @@ export class ControllerServiceTable {
return this.canRead(entity) ? this.nifiCommon.formatBundle(entity.component.bundle) : '';
}
getServiceLink(entity: ControllerServiceEntity): string[] {
if (entity.parentGroupId == null) {
return ['/settings', 'management-controller-services', entity.id];
} else {
return ['/process-groups', entity.parentGroupId, 'controller-services', entity.id];
}
goToControllerServiceClicked(entity: ControllerServiceEntity): void {
this.goToControllerService.next(entity);
}
isDisabled(entity: ControllerServiceEntity): boolean {
@ -259,6 +258,10 @@ export class ControllerServiceTable {
this.deleteControllerService.next(entity);
}
manageAccessPoliciesClicked(entity: ControllerServiceEntity): void {
this.manageAccessPolicies.next(entity);
}
changeVersionClicked(entity: ControllerServiceEntity) {
this.changeControllerServiceVersion.next(entity);
}
@ -275,10 +278,6 @@ export class ControllerServiceTable {
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
}
getPolicyLink(entity: ControllerServiceEntity): string[] {
return ['/access-policies', 'read', 'component', 'controller-services', entity.id];
}
select(entity: ControllerServiceEntity): void {
this.selectControllerService.next(entity);
}

View File

@ -166,7 +166,7 @@ export class EditControllerService extends TabbedDialog {
return this.nifiCommon.formatBundle(entity.component.bundle);
}
submitForm(postUpdateNavigation?: string[]) {
submitForm(postUpdateNavigation?: string[], postUpdateNavigationBoundary?: string[]) {
const payload: any = {
revision: this.client.getRevision(this.request.controllerService),
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
@ -188,7 +188,8 @@ export class EditControllerService extends TabbedDialog {
this.editControllerService.next({
payload,
postUpdateNavigation
postUpdateNavigation,
postUpdateNavigationBoundary
});
}

View File

@ -15,178 +15,188 @@
~ limitations under the License.
-->
<nav class="nifi-navigation select-none">
<div class="flex justify-between items-center h-16 pl-4">
<div class="flex">
<div class="h-16 w-28 mr-6 relative">
<img
ngSrc="assets/icons/nifi-logo.svg"
fill
priority
alt="NiFi Logo"
class="pointer"
[routerLink]="getCanvasLink()" />
</div>
<ng-content></ng-content>
</div>
@if (currentUser(); as user) {
<div class="flex justify-between items-center gap-x-1">
<div class="flex flex-col justify-between items-end gap-y-1">
@if (!user.anonymous) {
<div class="current-user">{{ user.identity }}</div>
}
@if (allowLogin(user)) {
<a href="#">log in</a>
}
@if (user.logoutSupported) {
<a (click)="logout()">log out</a>
}
<div class="flex flex-col">
<nav class="nifi-navigation select-none">
<div class="flex justify-between items-center h-16 pl-4">
<div class="flex">
<div class="h-16 w-28 mr-6 relative">
<img
ngSrc="assets/icons/nifi-logo.svg"
fill
priority
alt="NiFi Logo"
class="pointer"
[routerLink]="getCanvasLink()" />
</div>
<button
mat-button
[matMenuTriggerFor]="globalMenu"
class="h-16 w-16 flex items-center justify-center icon global-menu">
<i class="fa fa-navicon"></i>
</button>
<mat-menu #globalMenu="matMenu" xPosition="before">
<button mat-menu-item class="global-menu-item" [routerLink]="getCanvasLink()">
<i class="icon icon-drop primary-color mr-2"></i>
Canvas
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item" [routerLink]="['/summary']">
<i class="fa fa-table primary-color mr-2"></i>
Summary
</button>
<ng-content></ng-content>
</div>
@if (currentUser(); as user) {
<div class="flex justify-between items-center gap-x-1">
<div class="flex flex-col justify-between items-end gap-y-1">
@if (!user.anonymous) {
<div class="current-user">{{ user.identity }}</div>
}
@if (allowLogin(user)) {
<a href="#">log in</a>
}
@if (user.logoutSupported) {
<a (click)="logout()">log out</a>
}
</div>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/counters']"
[disabled]="!user.countersPermissions.canRead">
<i class="icon icon-counter primary-color mr-2"></i>
Counter
mat-button
[matMenuTriggerFor]="globalMenu"
class="h-16 w-16 flex items-center justify-center icon global-menu">
<i class="fa fa-navicon"></i>
</button>
<button mat-menu-item class="global-menu-item" [routerLink]="['/bulletins']">
<i class="fa fa-sticky-note-o primary-color mr-2"></i>
Bulletin Board
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/provenance']"
[disabled]="!user.provenancePermissions.canRead">
<i class="icon icon-provenance primary-color mr-2"></i>
Data Provenance
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/settings']"
[disabled]="!user.controllerPermissions.canRead">
<i class="fa fa-wrench primary-color mr-2"></i>
Controller Settings
</button>
<button mat-menu-item class="global-menu-item" [routerLink]="['/parameter-contexts']">
<i class="fa mr-2"></i>
Parameter Contexts
</button>
@if (clusterSummary()?.clustered) {
<mat-menu #globalMenu="matMenu" xPosition="before">
<button mat-menu-item class="global-menu-item" [routerLink]="getCanvasLink()">
<i class="icon icon-drop primary-color mr-2"></i>
Canvas
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item" [routerLink]="['/summary']">
<i class="fa fa-table primary-color mr-2"></i>
Summary
</button>
<button
mat-menu-item
class="global-menu-item"
[disabled]="!user.controllerPermissions.canRead"
[routerLink]="['/cluster']">
<i class="fa fa-cubes primary-color mr-2"></i>
Cluster
[routerLink]="['/counters']"
[disabled]="!user.countersPermissions.canRead">
<i class="icon icon-counter primary-color mr-2"></i>
Counter
</button>
}
<button mat-menu-item class="global-menu-item" [routerLink]="['/flow-configuration-history']">
<i class="fa fa-history primary-color mr-2"></i>
Flow Configuration History
</button>
<button mat-menu-item class="global-menu-item" (click)="viewNodeStatusHistory()">
<i class="fa fa-area-chart primary-color mr-2"></i>
Node Status History
</button>
<button
mat-menu-item
class="global-menu-item"
[disabled]="!user.systemPermissions.canRead"
(click)="viewSystemDiagnostics()">
<i class="fa mr-2"></i>
System Diagnostics
</button>
@if (flowConfiguration(); as flowConfig) {
@if (flowConfig.supportsManagedAuthorizer) {
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item" [routerLink]="['/bulletins']">
<i class="fa fa-sticky-note-o primary-color mr-2"></i>
Bulletin Board
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/provenance']"
[disabled]="!user.provenancePermissions.canRead">
<i class="icon icon-provenance primary-color mr-2"></i>
Data Provenance
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/settings']"
[disabled]="!user.controllerPermissions.canRead">
<i class="fa fa-wrench primary-color mr-2"></i>
Controller Settings
</button>
<button mat-menu-item class="global-menu-item" [routerLink]="['/parameter-contexts']">
<i class="fa mr-2"></i>
Parameter Contexts
</button>
@if (clusterSummary()?.clustered) {
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/users']"
[disabled]="!user.tenantsPermissions.canRead">
<i class="fa fa-users primary-color mr-2"></i>
Users
[disabled]="!user.controllerPermissions.canRead"
[routerLink]="['/cluster']">
<i class="fa fa-cubes primary-color mr-2"></i>
Cluster
</button>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/access-policies', 'global']"
[disabled]="
!user.tenantsPermissions.canRead ||
!user.policiesPermissions.canRead ||
!user.policiesPermissions.canWrite
">
<i class="fa fa-key primary-color mr-2"></i>
Policies
</button>
<mat-divider></mat-divider>
}
}
<button mat-menu-item class="global-menu-item" [routerLink]="['/documentation']">
<i class="fa fa-question-circle primary-color mr-2"></i>
Help
</button>
<button mat-menu-item class="global-menu-item" (click)="viewAbout()">
<i class="fa fa-info-circle primary-color mr-2"></i>
About
</button>
<button mat-menu-item [matMenuTriggerFor]="theming">
<i class="fa mr-2"></i>
Appearance
</button>
</mat-menu>
<mat-menu #theming="matMenu" xPosition="before">
<button mat-menu-item class="global-menu-item" (click)="toggleTheme(LIGHT_THEME)">
@if (theme === LIGHT_THEME) {
<i class="fa fa-check primary-color mr-2"></i>
}
@if (theme !== LIGHT_THEME) {
<button mat-menu-item class="global-menu-item" [routerLink]="['/flow-configuration-history']">
<i class="fa fa-history primary-color mr-2"></i>
Flow Configuration History
</button>
<button mat-menu-item class="global-menu-item" (click)="viewNodeStatusHistory()">
<i class="fa fa-area-chart primary-color mr-2"></i>
Node Status History
</button>
<button
mat-menu-item
class="global-menu-item"
[disabled]="!user.systemPermissions.canRead"
(click)="viewSystemDiagnostics()">
<i class="fa mr-2"></i>
System Diagnostics
</button>
@if (flowConfiguration(); as flowConfig) {
@if (flowConfig.supportsManagedAuthorizer) {
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/users']"
[disabled]="!user.tenantsPermissions.canRead">
<i class="fa fa-users primary-color mr-2"></i>
Users
</button>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/access-policies', 'global']"
[disabled]="
!user.tenantsPermissions.canRead ||
!user.policiesPermissions.canRead ||
!user.policiesPermissions.canWrite
">
<i class="fa fa-key primary-color mr-2"></i>
Policies
</button>
<mat-divider></mat-divider>
}
}
Light
</button>
<button mat-menu-item class="global-menu-item" (click)="toggleTheme(DARK_THEME)">
@if (theme === DARK_THEME) {
<i class="fa fa-check primary-color mr-2"></i>
}
@if (theme !== DARK_THEME) {
<button mat-menu-item class="global-menu-item" [routerLink]="['/documentation']">
<i class="fa fa-question-circle primary-color mr-2"></i>
Help
</button>
<button mat-menu-item class="global-menu-item" (click)="viewAbout()">
<i class="fa fa-info-circle primary-color mr-2"></i>
About
</button>
<button mat-menu-item [matMenuTriggerFor]="theming">
<i class="fa mr-2"></i>
}
Dark
</button>
<button mat-menu-item class="global-menu-item" (click)="toggleTheme(OS_SETTING)">
@if (theme === OS_SETTING || theme === null) {
<i class="fa fa-check primary-color mr-2"></i>
}
@if (theme !== OS_SETTING && theme !== null) {
<i class="fa mr-2"></i>
}
Device
</button>
</mat-menu>
</div>
}
</div>
</nav>
Appearance
</button>
</mat-menu>
<mat-menu #theming="matMenu" xPosition="before">
<button mat-menu-item class="global-menu-item" (click)="toggleTheme(LIGHT_THEME)">
@if (theme === LIGHT_THEME) {
<i class="fa fa-check primary-color mr-2"></i>
}
@if (theme !== LIGHT_THEME) {
<i class="fa mr-2"></i>
}
Light
</button>
<button mat-menu-item class="global-menu-item" (click)="toggleTheme(DARK_THEME)">
@if (theme === DARK_THEME) {
<i class="fa fa-check primary-color mr-2"></i>
}
@if (theme !== DARK_THEME) {
<i class="fa mr-2"></i>
}
Dark
</button>
<button mat-menu-item class="global-menu-item" (click)="toggleTheme(OS_SETTING)">
@if (theme === OS_SETTING || theme === null) {
<i class="fa fa-check primary-color mr-2"></i>
}
@if (theme !== OS_SETTING && theme !== null) {
<i class="fa mr-2"></i>
}
Device
</button>
</mat-menu>
</div>
}
</div>
</nav>
@if (backNavigation(); as navigation) {
<div class="pl-2 py-2">
<a [routerLink]="navigation.route">
<i class="fa fa-arrow-left primary-color mr-2"></i>
Back to {{ navigation.context }}
</a>
</div>
}
</div>

View File

@ -19,17 +19,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Navigation } from './navigation.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/current-user/current-user.reducer';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
import * as fromUser from '../../../state/current-user/current-user.reducer';
import * as fromNavigation from '../../../state/navigation/navigation.reducer';
import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
import * as fromClusterSummary from '../../../state/cluster-summary/cluster-summary.reducer';
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
import { selectLoginConfiguration } from '../../../state/login-configuration/login-configuration.selectors';
import * as fromLoginConfiguration from '../../../state/login-configuration/login-configuration.reducer';
import { currentUserFeatureKey } from '../../../state/current-user';
import { navigationFeatureKey } from '../../../state/navigation';
describe('Navigation', () => {
let component: Navigation;
@ -40,7 +42,10 @@ describe('Navigation', () => {
imports: [Navigation, HttpClientTestingModule, RouterTestingModule],
providers: [
provideMockStore({
initialState,
initialState: {
[currentUserFeatureKey]: fromUser.initialState,
[navigationFeatureKey]: fromNavigation.initialState
},
selectors: [
{
selector: selectCurrentUser,

View File

@ -46,6 +46,7 @@ import {
} from '../../../state/cluster-summary/cluster-summary.actions';
import { selectClusterSummary } from '../../../state/cluster-summary/cluster-summary.selectors';
import { selectLoginConfiguration } from '../../../state/login-configuration/login-configuration.selectors';
import { selectBackNavigation } from '../../../state/navigation/navigation.selectors';
@Component({
selector: 'navigation',
@ -70,10 +71,12 @@ export class Navigation implements OnInit, OnDestroy {
LIGHT_THEME: string = LIGHT_THEME;
DARK_THEME: string = DARK_THEME;
OS_SETTING: string = OS_SETTING;
currentUser = this.store.selectSignal(selectCurrentUser);
flowConfiguration = this.store.selectSignal(selectFlowConfiguration);
loginConfiguration = this.store.selectSignal(selectLoginConfiguration);
clusterSummary = this.store.selectSignal(selectClusterSummary);
backNavigation = this.store.selectSignal(selectBackNavigation);
constructor(
private store: Store<NiFiState>,