[NIFI-12563] configure reporting task (#8208)

* [NIFI-12563] configure reporting task

* move types to appropriate place

* address dialog resizing issue for reporting task configuration

* add configureReportingTask to reducer

* final touches

* remove unused inputs

* remove unused import

This closes #8208
This commit is contained in:
Scott Aslan 2024-01-09 11:48:19 -05:00 committed by GitHub
parent afc367dbe5
commit bbd8d7fd8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1210 additions and 26 deletions

View File

@ -57,7 +57,13 @@ const routes: Routes = [
children: [
{
path: ':id',
component: ReportingTasks
component: ReportingTasks,
children: [
{
path: 'edit',
component: ReportingTasks
}
]
}
]
},

View File

@ -16,11 +16,12 @@
*/
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Client } from '../../../service/client.service';
import { NiFiCommon } from '../../../service/nifi-common.service';
import {
ConfigureReportingTaskRequest,
CreateReportingTaskRequest,
DeleteReportingTaskRequest,
ReportingTaskEntity,
@ -91,7 +92,17 @@ export class ReportingTaskService {
return this.httpClient.put(`${this.stripProtocol(entity.uri)}/run-status`, payload);
}
// updateControllerConfig(controllerEntity: ControllerEntity): Observable<any> {
// return this.httpClient.put(`${ControllerServiceService.API}/controller/config`, controllerEntity);
// }
getPropertyDescriptor(id: string, propertyName: string, sensitive: boolean): Observable<any> {
const params: any = {
propertyName,
sensitive
};
return this.httpClient.get(`${ReportingTaskService.API}/reporting-tasks/${id}/descriptors`, {
params
});
}
updateReportingTask(configureReportingTask: ConfigureReportingTaskRequest): Observable<any> {
return this.httpClient.put(this.stripProtocol(configureReportingTask.uri), configureReportingTask.payload);
}
}

View File

@ -300,13 +300,13 @@ export class ManagementControllerServicesEffects {
})
.pipe(
take(1),
switchMap((createReponse) => {
switchMap((createResponse) => {
// dispatch an inline create service success action so the new service is in the state
this.store.dispatch(
ManagementControllerServicesActions.inlineCreateControllerServiceSuccess(
{
response: {
controllerService: createReponse
controllerService: createResponse
}
}
)
@ -321,7 +321,7 @@ export class ManagementControllerServicesEffects {
createServiceDialogReference.close();
return {
value: createReponse.id,
value: createResponse.id,
descriptor:
descriptorResponse.propertyDescriptor
};

View File

@ -38,6 +38,31 @@ export interface CreateReportingTaskSuccess {
reportingTask: ReportingTaskEntity;
}
export interface ConfigureReportingTaskRequest {
id: string;
uri: string;
payload: any;
postUpdateNavigation?: string[];
}
export interface ConfigureReportingTaskSuccess {
id: string;
reportingTask: ReportingTaskEntity;
postUpdateNavigation?: string[];
}
export interface ConfigureReportingTaskRequest {
id: string;
uri: string;
payload: any;
postUpdateNavigation?: string[];
}
export interface EditReportingTaskDialogRequest {
id: string;
reportingTask: ReportingTaskEntity;
}
export interface StartReportingTaskRequest {
reportingTask: ReportingTaskEntity;
}
@ -63,7 +88,7 @@ export interface DeleteReportingTaskSuccess {
}
export interface SelectReportingTaskRequest {
reportingTask: ReportingTaskEntity;
id: string;
}
export interface ReportingTaskEntity {

View File

@ -21,12 +21,15 @@ import {
CreateReportingTaskSuccess,
DeleteReportingTaskRequest,
DeleteReportingTaskSuccess,
EditReportingTaskDialogRequest,
LoadReportingTasksResponse,
SelectReportingTaskRequest,
StartReportingTaskRequest,
StartReportingTaskSuccess,
StopReportingTaskRequest,
StopReportingTaskSuccess
StopReportingTaskSuccess,
ConfigureReportingTaskRequest,
ConfigureReportingTaskSuccess
} from './index';
export const resetReportingTasksState = createAction('[Reporting Tasks] Reset Reporting Tasks State');
@ -38,6 +41,21 @@ export const loadReportingTasksSuccess = createAction(
props<{ response: LoadReportingTasksResponse }>()
);
export const openConfigureReportingTaskDialog = createAction(
'[Reporting Tasks] Open Reporting Task Dialog',
props<{ request: EditReportingTaskDialogRequest }>()
);
export const configureReportingTask = createAction(
'[Reporting Tasks] Configure Reporting Task',
props<{ request: ConfigureReportingTaskRequest }>()
);
export const configureReportingTaskSuccess = createAction(
'[Reporting Tasks] Configure Reporting Task Success',
props<{ response: ConfigureReportingTaskSuccess }>()
);
export const reportingTasksApiError = createAction(
'[Reporting Tasks] Load Reporting Tasks Error',
props<{ error: string }>()
@ -55,6 +73,11 @@ export const createReportingTaskSuccess = createAction(
props<{ response: CreateReportingTaskSuccess }>()
);
export const navigateToEditReportingTask = createAction(
'[Reporting Tasks] Navigate To Edit Reporting Task',
props<{ id: string }>()
);
export const startReportingTask = createAction(
'[Reporting Tasks] Start Reporting Task',
props<{ request: StartReportingTaskRequest }>()

View File

@ -18,7 +18,7 @@
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as ReportingTaskActions from './reporting-tasks.actions';
import { catchError, from, map, of, switchMap, take, tap, withLatestFrom } from 'rxjs';
import { catchError, from, map, NEVER, Observable, of, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../../state';
@ -27,13 +27,34 @@ import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.c
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 {
InlineServiceCreationRequest,
InlineServiceCreationResponse,
NewPropertyDialogRequest,
NewPropertyDialogResponse,
Property,
PropertyDescriptor,
UpdateControllerServiceRequest
} from '../../../../state/shared';
import { NewPropertyDialog } from '../../../../ui/common/new-property-dialog/new-property-dialog.component';
import { EditReportingTask } from '../../ui/reporting-tasks/edit-reporting-task/edit-reporting-task.component';
import { CreateReportingTaskSuccess } from './index';
import { ExtensionTypesService } from '../../../../service/extension-types.service';
import { CreateControllerService } from '../../../../ui/common/controller-service/create-controller-service/create-controller-service.component';
import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
import * as ManagementControllerServicesActions from '../management-controller-services/management-controller-services.actions';
import { Client } from '../../../../service/client.service';
@Injectable()
export class ReportingTasksEffects {
constructor(
private actions$: Actions,
private store: Store<NiFiState>,
private client: Client,
private reportingTaskService: ReportingTaskService,
private managementControllerServiceService: ManagementControllerServiceService,
private extensionTypesService: ExtensionTypesService,
private dialog: MatDialog,
private router: Router
) {}
@ -112,11 +133,11 @@ export class ReportingTasksEffects {
tap(() => {
this.dialog.closeAll();
}),
switchMap((response) =>
switchMap((response: CreateReportingTaskSuccess) =>
of(
ReportingTaskActions.selectReportingTask({
request: {
reportingTask: response.reportingTask
id: response.reportingTask.id
}
})
)
@ -175,6 +196,250 @@ export class ReportingTasksEffects {
)
);
navigateToEditReportingTask$ = createEffect(
() =>
this.actions$.pipe(
ofType(ReportingTaskActions.navigateToEditReportingTask),
map((action) => action.id),
tap((id) => {
this.router.navigate(['/settings', 'reporting-tasks', id, 'edit']);
})
),
{ dispatch: false }
);
openConfigureReportingTaskDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ReportingTaskActions.openConfigureReportingTaskDialog),
map((action) => action.request),
tap((request) => {
const taskId: string = request.id;
const editDialogReference = this.dialog.open(EditReportingTask, {
data: {
reportingTask: request.reportingTask
},
id: taskId,
panelClass: 'large-dialog'
});
editDialogReference.componentInstance.saving$ = this.store.select(selectSaving);
editDialogReference.componentInstance.createNewProperty = (
existingProperties: string[],
allowsSensitive: boolean
): Observable<Property> => {
const dialogRequest: NewPropertyDialogRequest = { existingProperties, allowsSensitive };
const newPropertyDialogReference = this.dialog.open(NewPropertyDialog, {
data: dialogRequest,
panelClass: 'small-dialog'
});
return newPropertyDialogReference.componentInstance.newProperty.pipe(
take(1),
switchMap((dialogResponse: NewPropertyDialogResponse) => {
return this.reportingTaskService
.getPropertyDescriptor(request.id, dialogResponse.name, dialogResponse.sensitive)
.pipe(
take(1),
map((response) => {
newPropertyDialogReference.close();
return {
property: dialogResponse.name,
value: null,
descriptor: response.propertyDescriptor
};
})
);
})
);
};
const goTo = (commands: string[], destination: string): void => {
if (editDialogReference.componentInstance.editReportingTaskForm.dirty) {
const saveChangesDialogReference = this.dialog.open(YesNoDialog, {
data: {
title: 'Controller Service Configuration',
message: `Save changes before going to this ${destination}?`
},
panelClass: 'small-dialog'
});
saveChangesDialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
editDialogReference.componentInstance.submitForm(commands);
});
saveChangesDialogReference.componentInstance.no.pipe(take(1)).subscribe(() => {
editDialogReference.close('ROUTED');
this.router.navigate(commands);
});
} else {
editDialogReference.close('ROUTED');
this.router.navigate(commands);
}
};
editDialogReference.componentInstance.goToService = (serviceId: string) => {
const commands: string[] = ['/settings', 'management-controller-services', serviceId];
goTo(commands, 'Controller Service');
};
editDialogReference.componentInstance.createNewService = (
request: InlineServiceCreationRequest
): Observable<InlineServiceCreationResponse> => {
const descriptor: PropertyDescriptor = request.descriptor;
// fetch all services that implement the requested service api
return this.extensionTypesService
.getImplementingControllerServiceTypes(
// @ts-ignore
descriptor.identifiesControllerService,
descriptor.identifiesControllerServiceBundle
)
.pipe(
take(1),
switchMap((implementingTypesResponse) => {
// show the create controller service dialog with the types that implemented the interface
const createServiceDialogReference = this.dialog.open(CreateControllerService, {
data: {
controllerServiceTypes: implementingTypesResponse.controllerServiceTypes
},
panelClass: 'medium-dialog'
});
return createServiceDialogReference.componentInstance.createControllerService.pipe(
take(1),
switchMap((controllerServiceType) => {
// typically this sequence would be implemented with ngrx actions, however we are
// currently in an edit session and we need to return both the value (new service id)
// and updated property descriptor so the table renders correctly
return this.managementControllerServiceService
.createControllerService({
revision: {
clientId: this.client.getClientId(),
version: 0
},
controllerServiceType: controllerServiceType.type,
controllerServiceBundle: controllerServiceType.bundle
})
.pipe(
take(1),
switchMap((createResponse) => {
// dispatch an inline create service success action so the new service is in the state
this.store.dispatch(
ManagementControllerServicesActions.inlineCreateControllerServiceSuccess(
{
response: {
controllerService: createResponse
}
}
)
);
// fetch an updated property descriptor
return this.reportingTaskService
.getPropertyDescriptor(taskId, descriptor.name, false)
.pipe(
take(1),
map((descriptorResponse) => {
createServiceDialogReference.close();
return {
value: createResponse.id,
descriptor:
descriptorResponse.propertyDescriptor
};
})
);
}),
catchError((error) => {
// TODO - show error
return NEVER;
})
);
})
);
})
);
};
editDialogReference.componentInstance.editReportingTask
.pipe(takeUntil(editDialogReference.afterClosed()))
.subscribe((updateControllerServiceRequest: UpdateControllerServiceRequest) => {
this.store.dispatch(
ReportingTaskActions.configureReportingTask({
request: {
id: request.reportingTask.id,
uri: request.reportingTask.uri,
payload: updateControllerServiceRequest.payload,
postUpdateNavigation: updateControllerServiceRequest.postUpdateNavigation
}
})
);
});
editDialogReference.afterClosed().subscribe((response) => {
if (response != 'ROUTED') {
this.store.dispatch(
ReportingTaskActions.selectReportingTask({
request: {
id: taskId
}
})
);
}
});
})
),
{ dispatch: false }
);
configureReportingTask$ = createEffect(() =>
this.actions$.pipe(
ofType(ReportingTaskActions.configureReportingTask),
map((action) => action.request),
switchMap((request) =>
from(this.reportingTaskService.updateReportingTask(request)).pipe(
map((response) =>
ReportingTaskActions.configureReportingTaskSuccess({
response: {
id: request.id,
reportingTask: response,
postUpdateNavigation: request.postUpdateNavigation
}
})
),
catchError((error) =>
of(
ReportingTaskActions.reportingTasksApiError({
error: error.error
})
)
)
)
)
)
);
configureReportingTaskSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(ReportingTaskActions.configureReportingTaskSuccess),
map((action) => action.response),
tap((response) => {
if (response.postUpdateNavigation) {
this.router.navigate(response.postUpdateNavigation);
this.dialog.getDialogById(response.id)?.close('ROUTED');
} else {
this.dialog.closeAll();
}
})
),
{ dispatch: false }
);
startReportingTask$ = createEffect(() =>
this.actions$.pipe(
ofType(ReportingTaskActions.startReportingTask),
@ -231,7 +496,7 @@ export class ReportingTasksEffects {
ofType(ReportingTaskActions.selectReportingTask),
map((action) => action.request),
tap((request) => {
this.router.navigate(['/settings', 'reporting-tasks', request.reportingTask.id]);
this.router.navigate(['/settings', 'reporting-tasks', request.id]);
})
),
{ dispatch: false }

View File

@ -18,6 +18,8 @@
import { createReducer, on } from '@ngrx/store';
import { ReportingTasksState } from './index';
import {
configureReportingTask,
configureReportingTaskSuccess,
createReportingTask,
createReportingTaskSuccess,
deleteReportingTask,
@ -61,7 +63,16 @@ export const reportingTasksReducer = createReducer(
error,
status: 'error' as const
})),
on(createReportingTask, deleteReportingTask, (state, { request }) => ({
on(configureReportingTaskSuccess, (state, { response }) => {
return produce(state, (draftState) => {
const componentIndex: number = draftState.reportingTasks.findIndex((f: any) => response.id === f.id);
if (componentIndex > -1) {
draftState.reportingTasks[componentIndex] = response.reportingTask;
}
draftState.saving = false;
});
}),
on(createReportingTask, deleteReportingTask, configureReportingTask, (state, { request }) => ({
...state,
saving: true
})),

View File

@ -17,7 +17,7 @@
import { createSelector } from '@ngrx/store';
import { selectSettingsState, SettingsState } from '../index';
import { reportingTasksFeatureKey, ReportingTasksState } from './index';
import { ReportingTaskEntity, reportingTasksFeatureKey, ReportingTasksState } from './index';
import { selectCurrentRoute } from '../../../../state/router/router.selectors';
export const selectReportingTasksState = createSelector(
@ -34,3 +34,18 @@ export const selectReportingTaskIdFromRoute = createSelector(selectCurrentRoute,
}
return null;
});
export const selectSingleEditedReportingTask = createSelector(selectCurrentRoute, (route) => {
if (route?.routeConfig?.path == 'edit') {
return route.params.id;
}
return null;
});
export const selectReportingTasks = createSelector(
selectReportingTasksState,
(state: ReportingTasksState) => state.reportingTasks
);
export const selectTask = (id: string) =>
createSelector(selectReportingTasks, (tasks: ReportingTaskEntity[]) => tasks.find((task) => id == task.id));

View File

@ -0,0 +1,111 @@
<!--
~ 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.
-->
<h2 mat-dialog-title>Edit Reporting Task</h2>
<form class="reporting-task-edit-form" [formGroup]="editReportingTaskForm">
<mat-dialog-content>
<mat-tab-group>
<mat-tab label="Settings">
<div class="tab-content py-4 flex gap-x-4">
<div class="w-full">
<div class="flex">
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput formControlName="name" type="text" />
</mat-form-field>
<mat-checkbox formControlName="state" class="pl-1"> Enabled </mat-checkbox>
</div>
<div class="flex flex-col mb-5">
<div>Id</div>
<div class="value">{{ request.reportingTask.id }}</div>
</div>
<div class="flex flex-col mb-5">
<div>Type</div>
<div class="value">{{ formatType(request.reportingTask) }}</div>
</div>
<div class="flex flex-col mb-5">
<div>Bundle</div>
<div class="value">{{ formatBundle(request.reportingTask) }}</div>
</div>
</div>
<div class="flex flex-col w-full">
<div>
<mat-form-field>
<mat-label>Scheduling Strategy</mat-label>
<mat-select
formControlName="schedulingStrategy"
(selectionChange)="schedulingStrategyChanged($event.value)">
<mat-option
*ngFor="let option of strategies"
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getPropertyTipData(option)"
[delayClose]="false">
{{ option.text }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>Run Schedule</mat-label>
<input
matInput
formControlName="schedulingPeriod"
(change)="schedulingPeriodChanged()"
type="text" />
</mat-form-field>
</div>
</div>
</div>
</mat-tab>
<mat-tab label="Properties">
<div class="tab-content py-4">
<property-table
formControlName="properties"
[createNewProperty]="createNewProperty"
[createNewService]="createNewService"
[goToService]="goToService"
[supportsSensitiveDynamicProperties]="
request.reportingTask.component.supportsSensitiveDynamicProperties
">
</property-table>
</div>
</mat-tab>
<mat-tab label="Comments">
<div class="tab-content py-4">
<mat-form-field>
<mat-label>Comments</mat-label>
<textarea matInput formControlName="comments" type="text" rows="5"></textarea>
</mat-form-field>
</div>
</mat-tab>
</mat-tab-group>
</mat-dialog-content>
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as saving">
<button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editReportingTaskForm.dirty || editReportingTaskForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-raised-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
</mat-dialog-actions>
</form>

View File

@ -0,0 +1,43 @@
/*
* 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.
*/
@use '@angular/material' as mat;
.reporting-task-edit-form {
@include mat.button-density(-1);
.mdc-dialog__content {
padding: 0 16px;
font-size: 14px;
.tab-content {
height: 475px;
overflow-y: auto;
}
}
.mat-mdc-form-field {
width: 100%;
}
.supports-reporting-tasks {
ul {
list-style: disc outside;
margin-left: 1em;
}
}
}

View File

@ -0,0 +1,398 @@
/*
* 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 { EditReportingTask } from './edit-reporting-task.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { EditReportingTaskDialogRequest } from '../../../state/reporting-tasks';
describe('EditReportingTask', () => {
let component: EditReportingTask;
let fixture: ComponentFixture<EditReportingTask>;
const data: EditReportingTaskDialogRequest = {
id: 'd5142be7-018c-1000-7105-2b1163fe0355',
reportingTask: {
revision: {
clientId: 'da266443-018c-1000-7dd0-9fee5271e3e1',
version: 22
},
id: 'd5142be7-018c-1000-7105-2b1163fe0355',
uri: 'https://localhost:8443/nifi-api/reporting-tasks/d5142be7-018c-1000-7105-2b1163fe0355',
permissions: {
canRead: true,
canWrite: true
},
bulletins: [],
component: {
id: 'd5142be7-018c-1000-7105-2b1163fe0355',
name: 'SiteToSiteMetricsReportingTask',
type: 'org.apache.nifi.reporting.SiteToSiteMetricsReportingTask',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-site-to-site-reporting-nar',
version: '2.0.0-SNAPSHOT'
},
state: 'DISABLED',
comments: 'iutfiugviugv',
persistsState: false,
restricted: false,
deprecated: false,
multipleVersionsAvailable: false,
supportsSensitiveDynamicProperties: false,
schedulingPeriod: '5 mins',
schedulingStrategy: 'TIMER_DRIVEN',
defaultSchedulingPeriod: {
TIMER_DRIVEN: '0 sec',
CRON_DRIVEN: '* * * * * ?'
},
properties: {
'Destination URL': null,
'Input Port Name': null,
'SSL Context Service': null,
'Instance URL': 'http://${hostname(true)}:8080/nifi',
'Compress Events': 'true',
'Communications Timeout': '30 secs',
's2s-transport-protocol': 'RAW',
's2s-http-proxy-hostname': null,
's2s-http-proxy-port': null,
's2s-http-proxy-username': null,
's2s-http-proxy-password': null,
'record-writer': null,
'include-null-values': 'false',
's2s-metrics-hostname': '${hostname(true)}',
's2s-metrics-application-id': 'nifi',
's2s-metrics-format': 'ambari-format'
},
descriptors: {
'Destination URL': {
name: 'Destination URL',
displayName: 'Destination URL',
description:
'The URL of the destination NiFi instance or, if clustered, a comma-separated list of address in the format of http(s)://host:port/nifi. This destination URL will only be used to initiate the Site-to-Site connection. The data sent by this reporting task will be load-balanced on all the nodes of the destination (if clustered).',
required: true,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables defined at JVM level and system properties',
dependencies: []
},
'Input Port Name': {
name: 'Input Port Name',
displayName: 'Input Port Name',
description: 'The name of the Input Port to deliver data to.',
required: true,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables defined at JVM level and system properties',
dependencies: []
},
'SSL Context Service': {
name: 'SSL Context Service',
displayName: 'SSL Context Service',
description:
'The SSL Context Service to use when communicating with the destination. If not specified, communications will not be secure.',
allowableValues: [
{
allowableValue: {
displayName: 'StandardRestrictedSSLContextService',
value: 'd636f255-018c-1000-42a8-822c72a22795'
},
canRead: true
}
],
required: false,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
identifiesControllerService: 'org.apache.nifi.ssl.RestrictedSSLContextService',
identifiesControllerServiceBundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-services-api-nar',
version: '2.0.0-SNAPSHOT'
},
dependencies: []
},
'Instance URL': {
name: 'Instance URL',
displayName: 'Instance URL',
description: 'The URL of this instance to use in the Content URI of each event.',
defaultValue: 'http://${hostname(true)}:8080/nifi',
required: true,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables defined at JVM level and system properties',
dependencies: []
},
'Compress Events': {
name: 'Compress Events',
displayName: 'Compress Events',
description: 'Indicates whether or not to compress the data being sent.',
defaultValue: 'true',
allowableValues: [
{
allowableValue: {
displayName: 'true',
value: 'true'
},
canRead: true
},
{
allowableValue: {
displayName: 'false',
value: 'false'
},
canRead: true
}
],
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
'Communications Timeout': {
name: 'Communications Timeout',
displayName: 'Communications Timeout',
description:
'Specifies how long to wait to a response from the destination before deciding that an error has occurred and canceling the transaction',
defaultValue: '30 secs',
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
's2s-transport-protocol': {
name: 's2s-transport-protocol',
displayName: 'Transport Protocol',
description: 'Specifies which transport protocol to use for Site-to-Site communication.',
defaultValue: 'RAW',
allowableValues: [
{
allowableValue: {
displayName: 'RAW',
value: 'RAW'
},
canRead: true
},
{
allowableValue: {
displayName: 'HTTP',
value: 'HTTP'
},
canRead: true
}
],
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
's2s-http-proxy-hostname': {
name: 's2s-http-proxy-hostname',
displayName: 'HTTP Proxy hostname',
description:
"Specify the proxy server's hostname to use. If not specified, HTTP traffics are sent directly to the target NiFi instance.",
required: false,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
's2s-http-proxy-port': {
name: 's2s-http-proxy-port',
displayName: 'HTTP Proxy port',
description:
"Specify the proxy server's port number, optional. If not specified, default port 80 will be used.",
required: false,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
's2s-http-proxy-username': {
name: 's2s-http-proxy-username',
displayName: 'HTTP Proxy username',
description: 'Specify an user name to connect to the proxy server, optional.',
required: false,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
's2s-http-proxy-password': {
name: 's2s-http-proxy-password',
displayName: 'HTTP Proxy password',
description: 'Specify an user password to connect to the proxy server, optional.',
required: false,
sensitive: true,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
'record-writer': {
name: 'record-writer',
displayName: 'Record Writer',
description: 'Specifies the Controller Service to use for writing out the records.',
allowableValues: [],
required: false,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
identifiesControllerService: 'org.apache.nifi.serialization.RecordSetWriterFactory',
identifiesControllerServiceBundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-services-api-nar',
version: '2.0.0-SNAPSHOT'
},
dependencies: []
},
'include-null-values': {
name: 'include-null-values',
displayName: 'Include Null Values',
description: 'Indicate if null values should be included in records. Default will be false',
defaultValue: 'false',
allowableValues: [
{
allowableValue: {
displayName: 'true',
value: 'true'
},
canRead: true
},
{
allowableValue: {
displayName: 'false',
value: 'false'
},
canRead: true
}
],
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
's2s-metrics-hostname': {
name: 's2s-metrics-hostname',
displayName: 'Hostname',
description: 'The Hostname of this NiFi instance to be included in the metrics',
defaultValue: '${hostname(true)}',
required: true,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables defined at JVM level and system properties',
dependencies: []
},
's2s-metrics-application-id': {
name: 's2s-metrics-application-id',
displayName: 'Application ID',
description: 'The Application ID to be included in the metrics',
defaultValue: 'nifi',
required: true,
sensitive: false,
dynamic: false,
supportsEl: true,
expressionLanguageScope: 'Environment variables defined at JVM level and system properties',
dependencies: []
},
's2s-metrics-format': {
name: 's2s-metrics-format',
displayName: 'Output Format',
description:
'The output format that will be used for the metrics. If Record Format is selected, a Record Writer must be provided. If Ambari Format is selected, the Record Writer property should be empty.',
defaultValue: 'ambari-format',
allowableValues: [
{
allowableValue: {
displayName: 'Ambari Format',
value: 'ambari-format',
description:
'Metrics will be formatted according to the Ambari Metrics API. See Additional Details in Usage documentation.'
},
canRead: true
},
{
allowableValue: {
displayName: 'Record Format',
value: 'record-format',
description:
'Metrics will be formatted using the Record Writer property of this reporting task. See Additional Details in Usage documentation to have the description of the default schema.'
},
canRead: true
}
],
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
}
},
validationErrors: [
"'Input Port Name' is invalid because Input Port Name is required",
"'Destination URL' is invalid because Destination URL is required"
],
validationStatus: 'INVALID',
activeThreadCount: 0,
extensionMissing: false
},
operatePermissions: {
canRead: true,
canWrite: true
},
status: {
runStatus: 'DISABLED',
validationStatus: 'INVALID',
activeThreadCount: 0
}
}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditReportingTask, BrowserAnimationsModule],
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
});
fixture = TestBed.createComponent(EditReportingTask);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,214 @@
/*
* 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, EventEmitter, Inject, Input, Output, signal } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatInputModule } from '@angular/material/input';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import { Client } from '../../../../../service/client.service';
import {
ControllerServiceReferencingComponent,
InlineServiceCreationRequest,
InlineServiceCreationResponse,
Parameter,
ParameterContextReferenceEntity,
Property,
PropertyTipInput,
SelectOption,
TextTipInput,
UpdateControllerServiceRequest,
UpdateReportingTaskRequest
} from '../../../../../state/shared';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { PropertyTable } from '../../../../../ui/common/property-table/property-table.component';
import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
import { EditReportingTaskDialogRequest, ReportingTaskEntity } from '../../../state/reporting-tasks';
import { ControllerServiceApi } from '../../../../../ui/common/controller-service/controller-service-api/controller-service-api.component';
import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive';
import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
@Component({
selector: 'edit-reporting-task',
standalone: true,
templateUrl: './edit-reporting-task.component.html',
imports: [
ReactiveFormsModule,
MatDialogModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
NgIf,
MatTabsModule,
MatOptionModule,
MatSelectModule,
NgForOf,
PropertyTable,
ControllerServiceApi,
AsyncPipe,
NifiSpinnerDirective,
MatTooltipModule,
NifiTooltipDirective
],
styleUrls: ['./edit-reporting-task.component.scss']
})
export class EditReportingTask {
@Input() createNewProperty!: (existingProperties: string[], allowsSensitive: boolean) => Observable<Property>;
@Input() createNewService!: (request: InlineServiceCreationRequest) => Observable<InlineServiceCreationResponse>;
@Input() goToService!: (serviceId: string) => void;
@Input() goToReferencingComponent!: (component: ControllerServiceReferencingComponent) => void;
@Input() saving$!: Observable<boolean>;
@Output() editReportingTask: EventEmitter<UpdateReportingTaskRequest> =
new EventEmitter<UpdateReportingTaskRequest>();
editReportingTaskForm: FormGroup;
schedulingStrategy: string;
cronDrivenSchedulingPeriod: string;
timerDrivenSchedulingPeriod: string;
strategies: SelectOption[] = [
{
text: 'Timer driven',
value: 'TIMER_DRIVEN',
description: 'Reporting task will be scheduled on an interval defined by the run schedule.'
},
{
text: 'CRON driven',
value: 'CRON_DRIVEN',
description:
'Reporting task will be scheduled to run on at specific times based on the specified CRON string.'
}
];
constructor(
@Inject(MAT_DIALOG_DATA) public request: EditReportingTaskDialogRequest,
private formBuilder: FormBuilder,
private client: Client,
private nifiCommon: NiFiCommon
) {
const serviceProperties: any = request.reportingTask.component.properties;
const properties: Property[] = Object.entries(serviceProperties).map((entry: any) => {
const [property, value] = entry;
return {
property,
value,
descriptor: request.reportingTask.component.descriptors[property]
};
});
const defaultSchedulingPeriod: any = request.reportingTask.component.defaultSchedulingPeriod;
this.schedulingStrategy = request.reportingTask.component.schedulingStrategy;
let schedulingPeriod: string;
if (this.schedulingStrategy === 'CRON_DRIVEN') {
this.cronDrivenSchedulingPeriod = request.reportingTask.component.schedulingPeriod;
this.timerDrivenSchedulingPeriod = defaultSchedulingPeriod['TIMER_DRIVEN'];
schedulingPeriod = this.cronDrivenSchedulingPeriod;
} else {
this.cronDrivenSchedulingPeriod = defaultSchedulingPeriod['CRON_DRIVEN'];
this.timerDrivenSchedulingPeriod = request.reportingTask.component.schedulingPeriod;
schedulingPeriod = this.timerDrivenSchedulingPeriod;
}
// build the form
this.editReportingTaskForm = this.formBuilder.group({
name: new FormControl(request.reportingTask.component.name, Validators.required),
state: new FormControl(request.reportingTask.component.state === 'STOPPED', Validators.required),
schedulingStrategy: new FormControl(
request.reportingTask.component.schedulingStrategy,
Validators.required
),
schedulingPeriod: new FormControl(schedulingPeriod, Validators.required),
properties: new FormControl(properties),
comments: new FormControl(request.reportingTask.component.comments)
});
}
formatType(entity: ReportingTaskEntity): string {
return this.nifiCommon.formatType(entity.component);
}
formatBundle(entity: ReportingTaskEntity): string {
return this.nifiCommon.formatBundle(entity.component.bundle);
}
submitForm(postUpdateNavigation?: string[]) {
const payload: any = {
revision: this.client.getRevision(this.request.reportingTask),
component: {
id: this.request.reportingTask.id,
name: this.editReportingTaskForm.get('name')?.value,
comments: this.editReportingTaskForm.get('comments')?.value,
schedulingStrategy: this.editReportingTaskForm.get('schedulingStrategy')?.value,
schedulingPeriod: this.editReportingTaskForm.get('schedulingPeriod')?.value,
state: this.editReportingTaskForm.get('state')?.value ? 'STOPPED' : 'DISABLED'
}
};
const propertyControl: AbstractControl | null = this.editReportingTaskForm.get('properties');
if (propertyControl && propertyControl.dirty) {
const properties: Property[] = propertyControl.value;
const values: { [key: string]: string | null } = {};
properties.forEach((property) => (values[property.property] = property.value));
payload.component.properties = values;
payload.component.sensitiveDynamicPropertyNames = properties
.filter((property) => property.descriptor.dynamic && property.descriptor.sensitive)
.map((property) => property.descriptor.name);
}
this.editReportingTask.next({
payload,
postUpdateNavigation
});
}
getPropertyTipData(option: SelectOption): TextTipInput {
return {
// @ts-ignore
text: option.description
};
}
schedulingStrategyChanged(value: string): void {
this.schedulingStrategy = value;
if (value === 'CRON_DRIVEN') {
this.editReportingTaskForm.get('schedulingPeriod')?.setValue(this.cronDrivenSchedulingPeriod);
} else {
this.editReportingTaskForm.get('schedulingPeriod')?.setValue(this.timerDrivenSchedulingPeriod);
}
}
schedulingPeriodChanged(): void {
if (this.schedulingStrategy === 'CRON_DRIVEN') {
this.cronDrivenSchedulingPeriod = this.editReportingTaskForm.get('schedulingPeriod')?.value;
} else {
this.timerDrivenSchedulingPeriod = this.editReportingTaskForm.get('schedulingPeriod')?.value;
}
}
protected readonly TextTip = TextTip;
}

View File

@ -104,7 +104,11 @@
*ngIf="canStop(item)"
(click)="stopClicked(item)"
title="Stop"></div>
<div class="pointer fa fa-pencil" *ngIf="canEdit(item)" title="Edit"></div>
<div
class="pointer fa fa-pencil"
*ngIf="canEdit(item)"
(click)="configureClicked(item, $event)"
title="Edit"></div>
<div
class="pointer fa fa-play"
*ngIf="canStart(item)"

View File

@ -52,6 +52,7 @@ export class ReportingTaskTable implements AfterViewInit {
@Output() selectReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() deleteReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() startReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() configureReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() stopReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
protected readonly TextTip = TextTip;
@ -226,6 +227,11 @@ export class ReportingTaskTable implements AfterViewInit {
this.deleteReportingTask.next(entity);
}
configureClicked(entity: ReportingTaskEntity, event: MouseEvent): void {
event.stopPropagation();
this.configureReportingTask.next(entity);
}
canViewState(entity: ReportingTaskEntity): boolean {
return this.canRead(entity) && this.canWrite(entity) && entity.component.persistsState === true;
}

View File

@ -30,6 +30,7 @@
<reporting-task-table
[selectedReportingTaskId]="selectedReportingTaskId$ | async"
[reportingTasks]="reportingTaskState.reportingTasks"
(configureReportingTask)="configureReportingTask($event)"
(selectReportingTask)="selectReportingTask($event)"
(deleteReportingTask)="deleteReportingTask($event)"
(stopReportingTask)="stopReportingTask($event)"

View File

@ -16,25 +16,30 @@
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Store } from '@ngrx/store';
import { filter, switchMap, take } from 'rxjs';
import { ReportingTaskEntity, ReportingTasksState } from '../../state/reporting-tasks';
import {
selectReportingTaskIdFromRoute,
selectReportingTasksState
selectReportingTasksState,
selectSingleEditedReportingTask,
selectTask
} from '../../state/reporting-tasks/reporting-tasks.selectors';
import {
loadReportingTasks,
navigateToEditReportingTask,
openConfigureReportingTaskDialog,
openNewReportingTaskDialog,
promptReportingTaskDeletion,
resetReportingTasksState,
selectReportingTask,
startReportingTask,
stopReportingTask
stopReportingTask,
selectReportingTask
} 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';
import { NiFiState } from '../../../../state';
import { state } from '@angular/animations';
@Component({
selector: 'reporting-tasks',
@ -46,7 +51,32 @@ export class ReportingTasks implements OnInit, OnDestroy {
selectedReportingTaskId$ = this.store.select(selectReportingTaskIdFromRoute);
currentUser$ = this.store.select(selectCurrentUser);
constructor(private store: Store<NiFiState>) {}
constructor(private store: Store<NiFiState>) {
this.store
.select(selectSingleEditedReportingTask)
.pipe(
filter((id: string) => id != null),
switchMap((id: string) =>
this.store.select(selectTask(id)).pipe(
filter((entity) => entity != null),
take(1)
)
),
takeUntilDestroyed()
)
.subscribe((entity) => {
if (entity) {
this.store.dispatch(
openConfigureReportingTaskDialog({
request: {
id: entity.id,
reportingTask: entity
}
})
);
}
});
}
ngOnInit(): void {
this.store.dispatch(loadReportingTasks());
@ -69,7 +99,7 @@ export class ReportingTasks implements OnInit, OnDestroy {
this.store.dispatch(
selectReportingTask({
request: {
reportingTask: entity
id: entity.id
}
})
);
@ -85,6 +115,14 @@ export class ReportingTasks implements OnInit, OnDestroy {
);
}
configureReportingTask(entity: ReportingTaskEntity): void {
this.store.dispatch(
navigateToEditReportingTask({
id: entity.id
})
);
}
startReportingTask(entity: ReportingTaskEntity): void {
this.store.dispatch(
startReportingTask({

View File

@ -23,10 +23,18 @@ import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
import { ReportingTaskTable } from './reporting-task-table/reporting-task-table.component';
import { ControllerServiceTable } from '../../../../ui/common/controller-service/controller-service-table/controller-service-table.component';
@NgModule({
declarations: [ReportingTasks, ReportingTaskTable],
exports: [ReportingTasks],
imports: [CommonModule, NgxSkeletonLoaderModule, MatSortModule, MatTableModule, NifiTooltipDirective]
imports: [
CommonModule,
NgxSkeletonLoaderModule,
MatSortModule,
MatTableModule,
NifiTooltipDirective,
ControllerServiceTable
]
})
export class ReportingTasksModule {}

View File

@ -115,6 +115,11 @@ export interface UpdateControllerServiceRequest {
postUpdateNavigation?: string[];
}
export interface UpdateReportingTaskRequest {
payload: any;
postUpdateNavigation?: string[];
}
export interface SetEnableControllerServiceDialogRequest {
id: string;
controllerService: ControllerServiceEntity;

View File

@ -39,8 +39,8 @@
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
[delayClose]="false"
>{{ option.text }}
[delayClose]="false">
{{ option.text }}
</mat-option>
</mat-select>
</mat-form-field>