NIFI-12958: Adding support for custom UIs (#8601)

* NIFI-12958:
- Adding support for custom UIs.
- Running NiFi dev server at context path /nf.
- Fixing link used when clicking the logo in the header.
- Updating titles and icons used for editing components in Settings for better consistency.
- Fixed JOLT advanced UI height.

* NIFI-12958:
- Fixing lint issue.

* NIFI-12958:
- Fixing test issue.

This closes #8601
This commit is contained in:
Matt Gilman 2024-04-05 11:19:05 -04:00 committed by GitHub
parent 7f5680d1fe
commit 2c706f5228
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 848 additions and 31 deletions

View File

@ -58,7 +58,7 @@
},
"development": {
"buildTarget": "nifi:build:development",
"servePath": "/nifi"
"servePath": "/nf"
}
},
"defaultConfiguration": "development"

View File

@ -18,7 +18,7 @@ const target = {
});
},
bypass: function (req) {
if (req.url.startsWith('/nifi/')) {
if (req.url.startsWith('/nf/')) {
return req.url;
}
}

View File

@ -15,13 +15,15 @@
~ limitations under the License.
-->
<div class="pb-5 flex flex-col h-screen justify-between">
<div class="flex flex-col h-screen">
<header class="nifi-header">
<navigation></navigation>
</header>
@if (frameSource) {
<iframe class="flex-1" [src]="frameSource"></iframe>
} @else {
<iframe class="flex-1" src="../nifi-docs/documentation"></iframe>
}
<div class="p-2 flex flex-1 bg-white">
@if (frameSource) {
<iframe class="flex-1" [src]="frameSource"></iframe>
} @else {
<iframe class="flex-1" src="../nifi-docs/documentation"></iframe>
}
</div>
</div>

View File

@ -29,6 +29,7 @@ import {
moveComponents,
navigateToComponent,
navigateToControllerServicesForProcessGroup,
navigateToAdvancedProcessorUi,
navigateToEditComponent,
navigateToEditCurrentProcessGroup,
navigateToManageComponentPolicies,
@ -364,6 +365,25 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
}
}
},
{
condition: (selection: any) => {
if (this.canvasUtils.canRead(selection) && this.canvasUtils.isProcessor(selection)) {
const selectionData = selection.datum();
return !!selectionData.component.config.customUiUrl;
}
return false;
},
clazz: 'fa fa-cogs',
text: 'Advanced',
action: (selection: any) => {
const selectionData = selection.datum();
this.store.dispatch(
navigateToAdvancedProcessorUi({
id: selectionData.id
})
);
}
},
{
condition: (selection: any) => {
return this.canvasUtils.isProcessGroup(selection) || selection.empty();

View File

@ -0,0 +1,80 @@
/*
* 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 { ActivatedRouteSnapshot, ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { catchError, EMPTY, map, of, switchMap, take } from 'rxjs';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { AdvancedUiParams } from '../../../../state/shared';
import { Client } from '../../../../service/client.service';
import { selectService } from '../../state/controller-services/controller-services.selectors';
import { ControllerServiceService } from '../controller-service.service';
import { HttpErrorResponse } from '@angular/common/http';
import { fullScreenError } from '../../../../state/error/error.actions';
export const controllerServiceAdvancedUiParamsResolver: ResolveFn<AdvancedUiParams> = (
route: ActivatedRouteSnapshot
) => {
const store: Store<NiFiState> = inject(Store);
const controllerServiceService: ControllerServiceService = inject(ControllerServiceService);
const client: Client = inject(Client);
// getting id parameter from activated route because ngrx router store
// is not initialized when this resolver executes
const id: string | null = route.paramMap.get('id');
if (!id) {
return EMPTY;
}
return store.select(selectService(id)).pipe(
switchMap((service) => {
if (service) {
return of(service);
} else {
return controllerServiceService.getControllerService(id).pipe(
catchError((errorResponse: HttpErrorResponse) => {
store.dispatch(
fullScreenError({
errorDetail: {
title: 'Unable to Open Advanced UI',
message: errorResponse.error
}
})
);
return EMPTY;
})
);
}
}),
map((entity) => {
const revision = client.getRevision(entity);
const editable = entity.status.runStatus === 'DISABLED';
return {
url: entity.component.customUiUrl,
id: entity.id,
clientId: revision.clientId,
revision: revision.version,
editable,
disconnectedNodeAcknowledged: false // TODO
} as AdvancedUiParams;
}),
take(1)
);
};

View File

@ -0,0 +1,81 @@
/*
* 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 { ActivatedRouteSnapshot, ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { catchError, EMPTY, map, of, switchMap, take } from 'rxjs';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { selectProcessor } from '../../state/flow/flow.selectors';
import { FlowService } from '../flow.service';
import { AdvancedUiParams } from '../../../../state/shared';
import { Client } from '../../../../service/client.service';
import { fullScreenError } from '../../../../state/error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
export const processorAdvancedUiParamsResolver: ResolveFn<AdvancedUiParams> = (route: ActivatedRouteSnapshot) => {
const store: Store<NiFiState> = inject(Store);
const flowService: FlowService = inject(FlowService);
const client: Client = inject(Client);
// getting id parameter from activated route because ngrx router store
// is not initialized when this resolver executes
const id: string | null = route.paramMap.get('id');
if (!id) {
return EMPTY;
}
return store.select(selectProcessor(id)).pipe(
switchMap((processor) => {
if (processor) {
return of(processor);
} else {
return flowService.getProcessor(id).pipe(
catchError((errorResponse: HttpErrorResponse) => {
store.dispatch(
fullScreenError({
errorDetail: {
title: 'Unable to Open Advanced UI',
message: errorResponse.error
}
})
);
return EMPTY;
})
);
}
}),
map((entity) => {
const revision = client.getRevision(entity);
const editable = !(
entity.status.aggregateSnapshot.runStatus === 'Running' ||
entity.status.aggregateSnapshot.activeThreadCount > 0
);
return {
url: entity.component.config.customUiUrl,
id: entity.id,
clientId: revision.clientId,
revision: revision.version,
editable,
disconnectedNodeAcknowledged: false // TODO
} as AdvancedUiParams;
}),
take(1)
);
};

View File

@ -72,6 +72,11 @@ export const navigateToEditService = createAction(
props<{ id: string }>()
);
export const navigateToAdvancedServiceUi = createAction(
'[Controller Services] Navigate To Advanced Service UI',
props<{ id: string }>()
);
export const openConfigureControllerServiceDialog = createAction(
'[Controller Services] Open Configure Controller Service Dialog',
props<{ request: EditControllerServiceDialogRequest }>()

View File

@ -178,6 +178,19 @@ export class ControllerServicesEffects {
{ dispatch: false }
);
navigateToAdvancedServiceUi$ = createEffect(
() =>
this.actions$.pipe(
ofType(ControllerServicesActions.navigateToAdvancedServiceUi),
map((action) => action.id),
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([id, processGroupId]) => {
this.router.navigate(['/process-groups', processGroupId, 'controller-services', id, 'advanced']);
})
),
{ dispatch: false }
);
openConfigureControllerServiceDialog$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -351,6 +351,11 @@ export const navigateToEditComponent = createAction(
props<{ request: OpenComponentDialogRequest }>()
);
export const navigateToAdvancedProcessorUi = createAction(
`${CANVAS_PREFIX} Navigate To Advanced Processor Ui`,
props<{ id: string }>()
);
export const navigateToManageComponentPolicies = createAction(
`${CANVAS_PREFIX} Navigate To Manage Component Policies`,
props<{ request: NavigateToManageComponentPoliciesRequest }>()

View File

@ -898,6 +898,19 @@ export class FlowEffects {
{ dispatch: false }
);
navigateToAdvancedProcessorUi$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowActions.navigateToAdvancedProcessorUi),
map((action) => action.id),
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
tap(([id, processGroupId]) => {
this.router.navigate(['/process-groups', processGroupId, ComponentType.Processor, id, 'advanced']);
})
),
{ dispatch: false }
);
navigateToEditCurrentProcessGroup$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -18,8 +18,16 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Canvas } from './canvas.component';
import { AdvancedUi } from '../../../../ui/common/advanced-ui/advanced-ui.component';
import { processorAdvancedUiParamsResolver } from '../../service/resolver/processor-advanced-ui-params.resolver';
import { ComponentType } from '../../../../state/shared';
const routes: Routes = [
{
path: `${ComponentType.Processor}/:id/advanced`,
resolve: { advancedUiParams: processorAdvancedUiParamsResolver },
component: AdvancedUi
},
{
path: '',
component: Canvas,

View File

@ -52,7 +52,7 @@
type="button"
[disabled]="!canConfigure(selection)"
(click)="configure(selection)">
<i class="fa fa-gear"></i>
<i class="fa fa-cog"></i>
</button>
@if (supportsManagedAuthorizer()) {
<button

View File

@ -18,8 +18,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ControllerServices } from './controller-services.component';
import { AdvancedUi } from '../../../../ui/common/advanced-ui/advanced-ui.component';
import { controllerServiceAdvancedUiParamsResolver } from '../../service/resolver/controller-service-advanced-ui-params.resolver';
const routes: Routes = [
{
path: ':id/advanced',
resolve: { advancedUiParams: controllerServiceAdvancedUiParamsResolver },
component: AdvancedUi
},
{
path: '',
component: ControllerServices,

View File

@ -49,6 +49,7 @@
(selectControllerService)="selectControllerService($event)"
(viewControllerServiceDocumentation)="viewControllerServiceDocumentation($event)"
(configureControllerService)="configureControllerService($event)"
(openAdvancedUi)="openAdvancedUi($event)"
(enableControllerService)="enableControllerService($event)"
(disableControllerService)="disableControllerService($event)"
(viewStateControllerService)="viewStateControllerService($event)"

View File

@ -17,7 +17,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { filter, switchMap, take, tap } from 'rxjs';
import { filter, Observable, switchMap, take, tap } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
selectControllerServiceIdFromRoute,
@ -29,6 +29,7 @@ import {
import { ControllerServicesState } from '../../state/controller-services';
import {
loadControllerServices,
navigateToAdvancedServiceUi,
navigateToEditService,
openConfigureControllerServiceDialog,
openDisableControllerServiceDialog,
@ -47,6 +48,7 @@ import { NiFiState } from '../../../../state';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
import { FlowConfiguration } from '../../../../state/flow-configuration';
@Component({
selector: 'controller-services',
@ -57,7 +59,9 @@ export class ControllerServices implements OnInit, OnDestroy {
serviceState$ = this.store.select(selectControllerServicesState);
selectedServiceId$ = this.store.select(selectControllerServiceIdFromRoute);
currentUser$ = this.store.select(selectCurrentUser);
flowConfiguration$ = this.store.select(selectFlowConfiguration).pipe(isDefinedAndNotNull());
flowConfiguration$: Observable<FlowConfiguration> = this.store
.select(selectFlowConfiguration)
.pipe(isDefinedAndNotNull());
private currentProcessGroupId!: string;
@ -182,6 +186,14 @@ export class ControllerServices implements OnInit, OnDestroy {
);
}
openAdvancedUi(entity: ControllerServiceEntity): void {
this.store.dispatch(
navigateToAdvancedServiceUi({
id: entity.id
})
);
}
enableControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
openEnableControllerServiceDialog({

View File

@ -26,8 +26,27 @@ import { RegistryClients } from '../ui/registry-clients/registry-clients.compone
import { ParameterProviders } from '../ui/parameter-providers/parameter-providers.component';
import { authorizationGuard } from '../../../service/guard/authorization.guard';
import { CurrentUser } from '../../../state/current-user';
import { AdvancedUi } from '../../../ui/common/advanced-ui/advanced-ui.component';
import { controllerServiceAdvancedUiParamsResolver } from '../service/resolver/controller-service-advanced-ui-params.resolver';
import { reportingTaskAdvancedUiParamsResolver } from '../service/resolver/reporting-task-advanced-ui-params.resolver';
import { parameterProviderAdvancedUiParamsResolver } from '../service/resolver/parameter-provider-advanced-ui-params.resolver';
const routes: Routes = [
{
path: 'management-controller-services/:id/advanced',
resolve: { advancedUiParams: controllerServiceAdvancedUiParamsResolver },
component: AdvancedUi
},
{
path: 'reporting-tasks/:id/advanced',
resolve: { advancedUiParams: reportingTaskAdvancedUiParamsResolver },
component: AdvancedUi
},
{
path: 'parameter-providers/:id/advanced',
resolve: { advancedUiParams: parameterProviderAdvancedUiParamsResolver },
component: AdvancedUi
},
{
path: '',
component: Settings,

View File

@ -58,6 +58,13 @@ export class ManagementControllerServiceService implements ControllerServiceCrea
});
}
getControllerService(id: string): Observable<any> {
const uiOnly: any = { uiOnly: true };
return this.httpClient.get(`${ManagementControllerServiceService.API}/controller-services/${id}`, {
params: uiOnly
});
}
getPropertyDescriptor(id: string, propertyName: string, sensitive: boolean): Observable<any> {
const params: any = {
propertyName,

View File

@ -45,6 +45,10 @@ export class ParameterProviderService implements PropertyDescriptorRetriever {
return this.httpClient.get(`${ParameterProviderService.API}/flow/parameter-providers`);
}
getParameterProvider(id: string): Observable<any> {
return this.httpClient.get(`${ParameterProviderService.API}/parameter-providers/${id}`);
}
createParameterProvider(request: CreateParameterProviderRequest) {
return this.httpClient.post(`${ParameterProviderService.API}/controller/parameter-providers`, {
revision: request.revision,

View File

@ -44,6 +44,10 @@ export class ReportingTaskService implements PropertyDescriptorRetriever {
return this.httpClient.get(`${ReportingTaskService.API}/flow/reporting-tasks`);
}
getReportingTask(id: string): Observable<any> {
return this.httpClient.get(`${ReportingTaskService.API}/reporting-tasks/${id}`);
}
createReportingTask(createReportingTask: CreateReportingTaskRequest): Observable<any> {
return this.httpClient.post(`${ReportingTaskService.API}/controller/reporting-tasks`, {
revision: createReportingTask.revision,

View File

@ -0,0 +1,82 @@
/*
* 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 { ActivatedRouteSnapshot, ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { catchError, EMPTY, map, of, switchMap, take } from 'rxjs';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { AdvancedUiParams } from '../../../../state/shared';
import { Client } from '../../../../service/client.service';
import { HttpErrorResponse } from '@angular/common/http';
import { fullScreenError } from '../../../../state/error/error.actions';
import { ManagementControllerServiceService } from '../management-controller-service.service';
import { selectService } from '../../state/management-controller-services/management-controller-services.selectors';
export const controllerServiceAdvancedUiParamsResolver: ResolveFn<AdvancedUiParams> = (
route: ActivatedRouteSnapshot
) => {
const store: Store<NiFiState> = inject(Store);
const managementControllerServiceService: ManagementControllerServiceService = inject(
ManagementControllerServiceService
);
const client: Client = inject(Client);
// getting id parameter from activated route because ngrx router store
// is not initialized when this resolver executes
const id: string | null = route.paramMap.get('id');
if (!id) {
return EMPTY;
}
return store.select(selectService(id)).pipe(
switchMap((service) => {
if (service) {
return of(service);
} else {
return managementControllerServiceService.getControllerService(id).pipe(
catchError((errorResponse: HttpErrorResponse) => {
store.dispatch(
fullScreenError({
errorDetail: {
title: 'Unable to Open Advanced UI',
message: errorResponse.error
}
})
);
return EMPTY;
})
);
}
}),
map((entity) => {
const revision = client.getRevision(entity);
const editable = entity.status.runStatus === 'DISABLED';
return {
url: entity.component.customUiUrl,
id: entity.id,
clientId: revision.clientId,
revision: revision.version,
editable,
disconnectedNodeAcknowledged: false // TODO
} as AdvancedUiParams;
}),
take(1)
);
};

View File

@ -0,0 +1,78 @@
/*
* 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 { ActivatedRouteSnapshot, ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { catchError, EMPTY, map, of, switchMap, take } from 'rxjs';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { AdvancedUiParams } from '../../../../state/shared';
import { Client } from '../../../../service/client.service';
import { HttpErrorResponse } from '@angular/common/http';
import { fullScreenError } from '../../../../state/error/error.actions';
import { ParameterProviderService } from '../parameter-provider.service';
import { selectParameterProvider } from '../../state/parameter-providers/parameter-providers.selectors';
export const parameterProviderAdvancedUiParamsResolver: ResolveFn<AdvancedUiParams> = (
route: ActivatedRouteSnapshot
) => {
const store: Store<NiFiState> = inject(Store);
const parameterProviderService: ParameterProviderService = inject(ParameterProviderService);
const client: Client = inject(Client);
// getting id parameter from activated route because ngrx router store
// is not initialized when this resolver executes
const id: string | null = route.paramMap.get('id');
if (!id) {
return EMPTY;
}
return store.select(selectParameterProvider(id)).pipe(
switchMap((parameterProvider) => {
if (parameterProvider) {
return of(parameterProvider);
} else {
return parameterProviderService.getParameterProvider(id).pipe(
catchError((errorResponse: HttpErrorResponse) => {
store.dispatch(
fullScreenError({
errorDetail: {
title: 'Unable to Open Advanced UI',
message: errorResponse.error
}
})
);
return EMPTY;
})
);
}
}),
map((entity) => {
const revision = client.getRevision(entity);
return {
url: entity.component.customUiUrl,
id: entity.id,
clientId: revision.clientId,
revision: revision.version,
editable: true,
disconnectedNodeAcknowledged: false // TODO
} as AdvancedUiParams;
}),
take(1)
);
};

View File

@ -0,0 +1,78 @@
/*
* 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 { ActivatedRouteSnapshot, ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { catchError, EMPTY, map, of, switchMap, take } from 'rxjs';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
import { AdvancedUiParams } from '../../../../state/shared';
import { Client } from '../../../../service/client.service';
import { HttpErrorResponse } from '@angular/common/http';
import { fullScreenError } from '../../../../state/error/error.actions';
import { ReportingTaskService } from '../reporting-task.service';
import { selectTask } from '../../state/reporting-tasks/reporting-tasks.selectors';
export const reportingTaskAdvancedUiParamsResolver: ResolveFn<AdvancedUiParams> = (route: ActivatedRouteSnapshot) => {
const store: Store<NiFiState> = inject(Store);
const reportingTaskService: ReportingTaskService = inject(ReportingTaskService);
const client: Client = inject(Client);
// getting id parameter from activated route because ngrx router store
// is not initialized when this resolver executes
const id: string | null = route.paramMap.get('id');
if (!id) {
return EMPTY;
}
return store.select(selectTask(id)).pipe(
switchMap((reportingTask) => {
if (reportingTask) {
return of(reportingTask);
} else {
return reportingTaskService.getReportingTask(id).pipe(
catchError((errorResponse: HttpErrorResponse) => {
store.dispatch(
fullScreenError({
errorDetail: {
title: 'Unable to Open Advanced UI',
message: errorResponse.error
}
})
);
return EMPTY;
})
);
}
}),
map((entity) => {
const revision = client.getRevision(entity);
const editable = entity.status.runStatus === 'STOPPED' || entity.status.runStatus === 'DISABLED';
return {
url: entity.component.customUiUrl,
id: entity.id,
clientId: revision.clientId,
revision: revision.version,
editable,
disconnectedNodeAcknowledged: false // TODO
} as AdvancedUiParams;
}),
take(1)
);
};

View File

@ -79,6 +79,11 @@ export const navigateToEditService = createAction(
props<{ id: string }>()
);
export const navigateToAdvancedServiceUi = createAction(
'[Controller Services] Navigate To Advanced Service UI',
props<{ id: string }>()
);
export const openConfigureControllerServiceDialog = createAction(
'[Management Controller Services] Open Configure Controller Service Dialog',
props<{ request: EditControllerServiceDialogRequest }>()

View File

@ -171,6 +171,18 @@ export class ManagementControllerServicesEffects {
{ dispatch: false }
);
navigateToAdvancedServiceUi$ = createEffect(
() =>
this.actions$.pipe(
ofType(ManagementControllerServicesActions.navigateToAdvancedServiceUi),
map((action) => action.id),
tap((id) => {
this.router.navigate(['/settings', 'management-controller-services', id, 'advanced']);
})
),
{ dispatch: false }
);
openConfigureControllerServiceDialog$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -55,6 +55,7 @@ export interface ParameterProvider {
affectedComponents: AffectedComponentEntity[];
bundle: Bundle;
comments: string;
customUiUrl?: string;
deprecated: boolean;
descriptors: { [key: string]: PropertyDescriptor };
extensionMissing: boolean;

View File

@ -87,6 +87,11 @@ export const navigateToEditParameterProvider = createAction(
props<{ id: string }>()
);
export const navigateToAdvancedParameterProviderUi = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Navigate To Advanced Parameter Provider UI`,
props<{ id: string }>()
);
export const navigateToFetchParameterProvider = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Navigate To Fetch Parameter Provider`,
props<{ id: string }>()

View File

@ -237,6 +237,18 @@ export class ParameterProvidersEffects {
{ dispatch: false }
);
navigateToAdvancedParameterProviderUi$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterProviderActions.navigateToAdvancedParameterProviderUi),
map((action) => action.id),
tap((id) => {
this.router.navigate(['settings', 'parameter-providers', id, 'advanced']);
})
),
{ dispatch: false }
);
navigateToFetchParameterProvider$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -83,6 +83,11 @@ export const navigateToEditReportingTask = createAction(
props<{ id: string }>()
);
export const navigateToAdvancedReportingTaskUi = createAction(
'[Reporting Tasks] Navigate To Advanced Reporting Task UI',
props<{ id: string }>()
);
export const startReportingTask = createAction(
'[Reporting Tasks] Start Reporting Task',
props<{ request: StartReportingTaskRequest }>()

View File

@ -215,6 +215,18 @@ export class ReportingTasksEffects {
{ dispatch: false }
);
navigateToAdvancedReportingTaskUi$ = createEffect(
() =>
this.actions$.pipe(
ofType(ReportingTaskActions.navigateToAdvancedReportingTaskUi),
map((action) => action.id),
tap((id) => {
this.router.navigate(['/settings', 'reporting-tasks', id, 'advanced']);
})
),
{ dispatch: false }
);
openConfigureReportingTaskDialog$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -123,7 +123,7 @@
<td mat-cell *matCellDef="let item">
<div class="flex items-center gap-x-3">
@if (canConfigure(item)) {
<div class="pointer fa fa-gear" (click)="configureClicked(item, $event)" title="Edit"></div>
<div class="pointer fa fa-cog" (click)="configureClicked(item, $event)" title="Edit"></div>
}
<!-- TODO - handle read only in configure component? -->
@if (canDisable(item)) {

View File

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

View File

@ -26,6 +26,7 @@ import {
} from '../../state/management-controller-services/management-controller-services.selectors';
import {
loadManagementControllerServices,
navigateToAdvancedServiceUi,
navigateToEditService,
openConfigureControllerServiceDialog,
openDisableControllerServiceDialog,
@ -132,6 +133,14 @@ export class ManagementControllerServices implements OnInit, OnDestroy {
);
}
openAdvancedUi(entity: ControllerServiceEntity): void {
this.store.dispatch(
navigateToAdvancedServiceUi({
id: entity.id
})
);
}
enableControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch(
openEnableControllerServiceDialog({

View File

@ -93,10 +93,16 @@
<div class="flex items-center gap-x-3">
@if (canConfigure(item)) {
<div
class="pointer fa fa-pencil"
class="pointer fa fa-cog"
(click)="configureClicked(item, $event)"
title="Edit"></div>
}
@if (hasAdvancedUi(item)) {
<div
class="pointer fa fa-cogs"
(click)="advancedClicked(item, $event)"
title="Advanced"></div>
}
@if (canFetch(item)) {
<div
class="pointer fa fa-arrow-circle-down"

View File

@ -76,6 +76,7 @@ export class ParameterProvidersTable {
new EventEmitter<ParameterProviderEntity>();
@Output() configureParameterProvider: EventEmitter<ParameterProviderEntity> =
new EventEmitter<ParameterProviderEntity>();
@Output() openAdvancedUi: EventEmitter<ParameterProviderEntity> = new EventEmitter<ParameterProviderEntity>();
@Output() deleteParameterProvider: EventEmitter<ParameterProviderEntity> =
new EventEmitter<ParameterProviderEntity>();
@Output() fetchParameterProvider: EventEmitter<ParameterProviderEntity> =
@ -100,6 +101,10 @@ export class ParameterProvidersTable {
return this.canRead(entity) && this.canWrite(entity);
}
hasAdvancedUi(entity: ParameterProviderEntity): boolean {
return this.canRead(entity) && !!entity.component.customUiUrl;
}
canDelete(entity: ParameterProviderEntity): boolean {
return (
this.canRead(entity) &&
@ -194,6 +199,11 @@ export class ParameterProvidersTable {
this.configureParameterProvider.next(entity);
}
advancedClicked(entity: ParameterProviderEntity, event: MouseEvent) {
event.stopPropagation();
this.openAdvancedUi.next(entity);
}
fetchClicked(entity: ParameterProviderEntity, event: MouseEvent) {
event.stopPropagation();
this.fetchParameterProvider.next(entity);

View File

@ -41,6 +41,7 @@
[selectedParameterProviderId]="selectedParameterProviderId$ | async"
(deleteParameterProvider)="deleteParameterProvider($event)"
(configureParameterProvider)="openConfigureParameterProviderDialog($event)"
(openAdvancedUi)="openAdvancedUi($event)"
(fetchParameterProvider)="fetchParameterProviderParameters($event)"
(viewParameterProviderDocumentation)="viewParameterProviderDocumentation($event)"
(selectParameterProvider)="selectParameterProvider($event)"></parameter-providers-table>

View File

@ -131,6 +131,14 @@ export class ParameterProviders implements OnInit, OnDestroy {
);
}
openAdvancedUi(parameterProvider: ParameterProviderEntity) {
this.store.dispatch(
ParameterProviderActions.navigateToAdvancedParameterProviderUi({
id: parameterProvider.id
})
);
}
viewParameterProviderDocumentation(parameterProvider: ParameterProviderEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({

View File

@ -102,9 +102,9 @@
<div class="flex items-center gap-x-3">
@if (canConfigure(item)) {
<div
class="pointer fa fa-pencil"
class="pointer fa fa-cog"
(click)="configureClicked(item, $event)"
title="Configure"></div>
title="Edit"></div>
}
@if (canDelete(item)) {
<div class="pointer fa fa-trash" (click)="deleteClicked(item, $event)" title="Delete"></div>

View File

@ -118,10 +118,13 @@
<div class="pointer fa fa-stop" (click)="stopClicked(item)" title="Stop"></div>
}
@if (canEdit(item)) {
<div class="pointer fa fa-cog" (click)="configureClicked(item, $event)" title="Edit"></div>
}
@if (hasAdvancedUi(item)) {
<div
class="pointer fa fa-pencil"
(click)="configureClicked(item, $event)"
title="Edit"></div>
class="pointer fa fa-cogs"
(click)="advancedClicked(item, $event)"
title="Advanced"></div>
}
@if (canStart(item)) {
<div class="pointer fa fa-play" (click)="startClicked(item)" title="Start"></div>

View File

@ -54,6 +54,7 @@ export class ReportingTaskTable {
@Output() deleteReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() startReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() configureReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() openAdvancedUi: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() viewStateReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() stopReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@ -197,6 +198,15 @@ export class ReportingTaskTable {
return this.canRead(entity) && this.canWrite(entity) && this.isStoppedOrDisabled(entity);
}
hasAdvancedUi(entity: ReportingTaskEntity): boolean {
return this.canRead(entity) && !!entity.component.customUiUrl;
}
advancedClicked(entity: ReportingTaskEntity, event: MouseEvent): void {
event.stopPropagation();
this.openAdvancedUi.next(entity);
}
canStart(entity: ReportingTaskEntity): boolean {
return this.canOperate(entity) && this.isStopped(entity) && this.isValid(entity);
}
@ -205,10 +215,6 @@ export class ReportingTaskTable {
this.startReportingTask.next(entity);
}
canConfigure(entity: ReportingTaskEntity): boolean {
return this.canRead(entity) && this.canWrite(entity) && this.isDisabled(entity);
}
canChangeVersion(entity: ReportingTaskEntity): boolean {
return (
(this.isDisabled(entity) || this.isStopped(entity)) &&

View File

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

View File

@ -35,7 +35,8 @@ import {
resetReportingTasksState,
startReportingTask,
stopReportingTask,
selectReportingTask
selectReportingTask,
navigateToAdvancedReportingTaskUi
} from '../../state/reporting-tasks/reporting-tasks.actions';
import { initialState } from '../../state/reporting-tasks/reporting-tasks.reducer';
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
@ -111,6 +112,14 @@ export class ReportingTasks implements OnInit, OnDestroy {
);
}
openAdvancedUi(entity: ReportingTaskEntity): void {
this.store.dispatch(
navigateToAdvancedReportingTaskUi({
id: entity.id
})
);
}
viewReportingTaskDocumentation(entity: ReportingTaskEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { routerReducer, RouterReducerState } from '@ngrx/router-store';
import { routerReducer, RouterReducerState, DEFAULT_ROUTER_FEATURENAME } from '@ngrx/router-store';
import { ActionReducerMap } from '@ngrx/store';
import { CurrentUserState, currentUserFeatureKey } from './current-user';
import { currentUserReducer } from './current-user/current-user.reducer';
@ -41,7 +41,7 @@ import { clusterSummaryFeatureKey, ClusterSummaryState } from './cluster-summary
import { clusterSummaryReducer } from './cluster-summary/cluster-summary.reducer';
export interface NiFiState {
router: RouterReducerState;
[DEFAULT_ROUTER_FEATURENAME]: RouterReducerState;
[errorFeatureKey]: ErrorState;
[currentUserFeatureKey]: CurrentUserState;
[extensionTypesFeatureKey]: ExtensionTypesState;
@ -56,7 +56,7 @@ export interface NiFiState {
}
export const rootReducers: ActionReducerMap<NiFiState> = {
router: routerReducer,
[DEFAULT_ROUTER_FEATURENAME]: routerReducer,
[errorFeatureKey]: errorReducer,
[currentUserFeatureKey]: currentUserReducer,
[extensionTypesFeatureKey]: extensionTypesReducer,

View File

@ -60,6 +60,15 @@ export interface EditParameterResponse {
parameter: Parameter;
}
export interface AdvancedUiParams {
url: string;
id: string;
revision: number;
clientId: string;
editable: boolean;
disconnectedNodeAcknowledged: boolean;
}
export interface UserEntity {
id: string;
permissions: Permissions;

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.
-->
<div class="flex flex-col h-screen">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="p-2 flex flex-1 bg-white">
@if (frameSource) {
<iframe class="flex-1" [src]="frameSource"></iframe>
} @else {
<div>Unable to open Advanced configuration UI.</div>
}
</div>
</div>

View File

@ -0,0 +1,16 @@
/*
* 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.
*/

View File

@ -0,0 +1,51 @@
/*
* 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 { ComponentFixture, TestBed } from '@angular/core/testing';
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';
describe('AdvancedUi', () => {
let component: AdvancedUi;
let fixture: ComponentFixture<AdvancedUi>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [AdvancedUi, HttpClientTestingModule, RouterTestingModule, MockNavigation],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(AdvancedUi);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

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 { Component, SecurityContext } from '@angular/core';
import { NiFiState } from '../../../state';
import { Store } from '@ngrx/store';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { HttpParams } from '@angular/common/http';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Navigation } from '../navigation/navigation.component';
import { selectRouteData } from '../../../state/router/router.selectors';
import { AdvancedUiParams, isDefinedAndNotNull } from '../../../state/shared';
@Component({
selector: 'advanced-ui',
standalone: true,
templateUrl: './advanced-ui.component.html',
imports: [Navigation],
styleUrls: ['./advanced-ui.component.scss']
})
export class AdvancedUi {
frameSource!: SafeResourceUrl | null;
constructor(
private store: Store<NiFiState>,
private domSanitizer: DomSanitizer
) {
this.store
.select(selectRouteData)
.pipe(takeUntilDestroyed(), isDefinedAndNotNull())
.subscribe((data) => {
if (data['advancedUiParams']) {
this.frameSource = this.getFrameSource(data['advancedUiParams']);
}
});
}
private getFrameSource(params: AdvancedUiParams): SafeResourceUrl | null {
const queryParams: string = new HttpParams()
.set('id', params.id)
.set('revision', params.revision)
.set('clientId', params.clientId)
.set('editable', params.editable)
.set('disconnectedNodeAcknowledged', params.disconnectedNodeAcknowledged)
.toString();
const url = `${params.url}?${queryParams}`;
const sanitizedUrl = this.domSanitizer.sanitize(SecurityContext.URL, url);
if (sanitizedUrl) {
return this.domSanitizer.bypassSecurityTrustResourceUrl(sanitizedUrl);
}
return null;
}
}

View File

@ -122,14 +122,20 @@
<div class="flex items-center gap-x-3">
@if (canConfigure(item)) {
<div
class="pointer fa fa-gear"
class="pointer fa fa-cog"
(click)="configureClicked(item, $event)"
title="Configure"></div>
title="Edit"></div>
}
@if (hasAdvancedUi(item)) {
<div
class="pointer fa fa-cogs"
(click)="advancedClicked(item, $event)"
title="Advanced"></div>
}
<!-- TODO - handle read only in configure component? -->
@if (canDisable(item)) {
<div
class="pointer fa icon icon-enable-false"
class="pointer icon icon-enable-false"
(click)="disableClicked(item)"
title="Disable"></div>
}

View File

@ -78,6 +78,7 @@ export class ControllerServiceTable {
new EventEmitter<ControllerServiceEntity>();
@Output() configureControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@Output() openAdvancedUi: EventEmitter<ControllerServiceEntity> = new EventEmitter<ControllerServiceEntity>();
@Output() enableControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@Output() disableControllerService: EventEmitter<ControllerServiceEntity> =
@ -220,6 +221,15 @@ export class ControllerServiceTable {
this.configureControllerService.next(entity);
}
hasAdvancedUi(entity: ControllerServiceEntity): boolean {
return this.canRead(entity) && !!entity.component.customUiUrl;
}
advancedClicked(entity: ControllerServiceEntity, event: MouseEvent): void {
event.stopPropagation();
this.openAdvancedUi.next(entity);
}
canEnable(entity: ControllerServiceEntity): boolean {
const userAuthorized: boolean = this.canRead(entity) && this.canOperate(entity);
return userAuthorized && this.isDisabled(entity) && entity.status.validationStatus === 'VALID';

View File

@ -25,7 +25,7 @@
priority
alt="NiFi Logo"
class="pointer"
[routerLink]="['/']" />
[routerLink]="getCanvasLink()" />
</div>
<ng-content></ng-content>
</div>

View File

@ -40,7 +40,7 @@ div.code-mirror-editor.trans {
.CodeMirror {
border: 1px solid #eee;
height: 40vh;
height: 30vh;
}
.info {
@ -65,6 +65,7 @@ div.scrollable{
bottom: 0;
left: 0;
padding: 22px;
overflow: auto;
}
#addButton {