NIFI-13062: Read only configuration dialogs (#8665)

* NIFI-13062:
- Updating Configuration dialogs to support a read only mode for Processors, Controller Services, Reporting Tasks, Ports, Connections, Process Groups, Remote Process Groups, Flow Analysis Rules, and Parameter Contexts.
- For extensions points rendered the type in the header.

* NIFI-13062:
- Addressing review feedback.

This closes #8665
This commit is contained in:
Matt Gilman 2024-04-19 12:42:31 -04:00 committed by GitHub
parent 8c3c1eea31
commit 36c0ec4d7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
66 changed files with 836 additions and 415 deletions

View File

@ -37,7 +37,7 @@ export class QuickSelectBehavior {
const selection: any = this.canvasUtils.getSelection();
const selectionData: any = selection.datum();
if (this.canvasUtils.isConfigurable(selection)) {
if (this.canvasUtils.isConfigurable(selection) || this.canvasUtils.hasDetails(selection)) {
// show configuration dialog
this.store.dispatch(
navigateToEditComponent({
@ -47,9 +47,6 @@ export class QuickSelectBehavior {
}
})
);
} else if (this.canvasUtils.hasDetails(selection)) {
// TODO - show details (read only)... update Edit Forms to support readonly directive
// nfActions.showDetails(selection);
}
// stop propagation and prevent default

View File

@ -435,6 +435,28 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
}
}
},
{
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.hasDetails(selection);
},
clazz: 'fa fa-gear',
text: 'View configuration',
action: (selection: d3.Selection<any, any, any, any>) => {
if (selection.empty()) {
this.store.dispatch(navigateToEditCurrentProcessGroup());
} else {
const selectionData = selection.datum();
this.store.dispatch(
navigateToEditComponent({
request: {
type: selectionData.type,
id: selectionData.id
}
})
);
}
}
},
{
condition: (selection: any) => {
if (this.canvasUtils.canRead(selection) && this.canvasUtils.isProcessor(selection)) {
@ -481,17 +503,6 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
}
}
},
{
condition: (selection: any) => {
// TODO - hasDetails
return false;
},
clazz: 'fa fa-gear',
text: 'View configuration',
action: (selection: any) => {
// TODO - showDetails... Can we support read only and configurable in the same dialog/form?
}
},
{
condition: (selection: any) => {
// TODO - hasParameterContext

View File

@ -248,12 +248,35 @@ export class CanvasUtils {
return selectionSize === readableSize;
}
/**
* Determines whether the specified Processor, Input Port, or Output Port currently supports modification.
*
* @param entity
*/
public runnableSupportsModification(entity: any): boolean {
return !(
entity.status.aggregateSnapshot.runStatus === 'Running' ||
entity.status.aggregateSnapshot.activeThreadCount > 0
);
}
/**
* Determines whether the specified Remote Process Group currently supports modification.
*
* @param entity
*/
public remoteProcessGroupSupportsModification(entity: any): boolean {
return !(
entity.status.transmissionStatus === 'Transmitting' || entity.status.aggregateSnapshot.activeThreadCount > 0
);
}
/**
* Determines whether the specified selection is in a state to support modification.
*
* @argument {selection} selection The selection
*/
public supportsModification(selection: any): boolean {
private supportsModification(selection: d3.Selection<any, any, any, any>): boolean {
if (selection.size() !== 1) {
return false;
}
@ -263,15 +286,9 @@ export class CanvasUtils {
let supportsModification = false;
if (this.isProcessor(selection) || this.isInputPort(selection) || this.isOutputPort(selection)) {
supportsModification = !(
selectionData.status.aggregateSnapshot.runStatus === 'Running' ||
selectionData.status.aggregateSnapshot.activeThreadCount > 0
);
supportsModification = this.runnableSupportsModification(selectionData);
} else if (this.isRemoteProcessGroup(selection)) {
supportsModification = !(
selectionData.status.transmissionStatus === 'Transmitting' ||
selectionData.status.aggregateSnapshot.activeThreadCount > 0
);
supportsModification = this.remoteProcessGroupSupportsModification(selectionData);
} else if (this.isProcessGroup(selection)) {
supportsModification = true;
} else if (this.isFunnel(selection)) {
@ -383,20 +400,26 @@ export class CanvasUtils {
* @param selection
*/
public hasDetails(selection: any): boolean {
if (selection.empty()) {
return this.canvasPermissions.canRead && !this.canvasPermissions.canWrite;
}
// ensure the correct number of components are selected
if (selection.size() !== 1) {
if (selection.size() > 1) {
return false;
}
if (!this.canRead(selection)) {
return false;
}
if (this.canModify(selection)) {
if (
this.isProcessor(selection) ||
this.isInputPort(selection) ||
this.isOutputPort(selection) ||
this.isRemoteProcessGroup(selection) ||
this.isProcessGroup(selection) ||
this.isConnection(selection)
) {
return !this.isConfigurable(selection);
@ -404,10 +427,11 @@ export class CanvasUtils {
} else {
return (
this.isProcessor(selection) ||
this.isConnection(selection) ||
this.isInputPort(selection) ||
this.isOutputPort(selection) ||
this.isRemoteProcessGroup(selection)
this.isRemoteProcessGroup(selection) ||
this.isProcessGroup(selection) ||
this.isConnection(selection)
);
}
@ -1830,11 +1854,7 @@ export class CanvasUtils {
if (this.isProcessor(selection)) {
const data = selection.datum();
const supportsModification = !(
data.status.aggregateSnapshot.runStatus === 'Running' ||
data.status.aggregateSnapshot.activeThreadCount > 0
);
const supportsModification = this.runnableSupportsModification(data);
return supportsModification && data.component.multipleVersionsAvailable;
}
return false;

View File

@ -68,7 +68,9 @@ import {
selectCopiedSnippet,
selectCurrentParameterContext,
selectCurrentProcessGroupId,
selectInputPort,
selectMaxZIndex,
selectOutputPort,
selectParentProcessGroupId,
selectProcessGroup,
selectProcessor,
@ -1294,6 +1296,12 @@ export class FlowEffects {
editDialogReference.componentInstance.selectProcessor = (id: string) => {
return this.store.select(selectProcessor(id));
};
editDialogReference.componentInstance.selectInputPort = (id: string) => {
return this.store.select(selectInputPort(id));
};
editDialogReference.componentInstance.selectOutputPort = (id: string) => {
return this.store.select(selectOutputPort(id));
};
editDialogReference.componentInstance.selectProcessGroup = (id: string) => {
return this.store.select(selectProcessGroup(id));
};

View File

@ -29,11 +29,12 @@
} @else {
<mat-form-field>
<mat-label>To Input</mat-label>
<mat-select [(ngModel)]="selectedInputPort" (selectionChange)="handleChanged()" [disabled]="isDisabled">
<mat-select [(ngModel)]="selectedInputPort" (selectionChange)="handleChanged()">
@for (item of inputPortItems; track item) {
@if (item.description) {
<mat-option
[value]="item.value"
[disabled]="isDisabled"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(item)"
@ -41,7 +42,7 @@
>{{ item.text }}</mat-option
>
} @else {
<mat-option [value]="item.value">{{ item.text }}</mat-option>
<mat-option [value]="item.value" [disabled]="isDisabled">{{ item.text }}</mat-option>
}
}
</mat-select>

View File

@ -49,10 +49,12 @@ import { SelectOption, TextTipInput } from '../../../../../../../../state/shared
})
export class DestinationProcessGroup implements ControlValueAccessor {
@Input() set processGroup(processGroup: any) {
if (processGroup.permissions.canRead) {
this.groupName = processGroup.component.name;
} else {
this.groupName = processGroup.id;
if (processGroup) {
if (processGroup.permissions.canRead) {
this.groupName = processGroup.component.name;
} else {
this.groupName = processGroup.id;
}
}
}
@Input() set inputPorts(inputPorts: any[]) {

View File

@ -26,12 +26,12 @@
} @else {
<mat-form-field>
<mat-label>To Input</mat-label>
<mat-select [(ngModel)]="selectedInputPort" (selectionChange)="handleChanged()" [disabled]="isDisabled">
<mat-select [(ngModel)]="selectedInputPort" (selectionChange)="handleChanged()">
@for (item of inputPortItems; track item) {
@if (item.description) {
<mat-option
[value]="item.value"
[disabled]="item.disabled == true"
[disabled]="item.disabled == true || isDisabled"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(item)"
@ -39,7 +39,9 @@
>{{ item.text }}</mat-option
>
} @else {
<mat-option [value]="item.value" [disabled]="item.disabled == true">{{ item.text }}</mat-option>
<mat-option [value]="item.value" [disabled]="item.disabled == true || isDisabled">{{
item.text
}}</mat-option>
}
}
</mat-select>

View File

@ -49,23 +49,25 @@ import { SelectOption, TextTipInput } from '../../../../../../../../state/shared
})
export class DestinationRemoteProcessGroup implements ControlValueAccessor {
@Input() set remoteProcessGroup(remoteProcessGroup: any) {
const rpg = remoteProcessGroup.component;
const inputPorts: any[] = rpg.contents.inputPorts;
if (remoteProcessGroup) {
const rpg = remoteProcessGroup.component;
const inputPorts: any[] = rpg.contents.inputPorts;
if (inputPorts) {
this.noPorts = inputPorts.length == 0;
if (inputPorts) {
this.noPorts = inputPorts.length == 0;
this.inputPortItems = inputPorts.map((inputPort) => {
return {
value: inputPort.id,
text: inputPort.name,
description: inputPort.comments,
disabled: inputPort.exists === false
};
});
this.inputPortItems = inputPorts.map((inputPort) => {
return {
value: inputPort.id,
text: inputPort.name,
description: inputPort.comments,
disabled: inputPort.exists === false
};
});
}
this.groupName = rpg.name;
}
this.groupName = rpg.name;
}
protected readonly TextTip = TextTip;

View File

@ -15,7 +15,9 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Edit Connection</h2>
<h2 mat-dialog-title>
{{ connectionReadonly || sourceReadonly || destinationReadonly ? 'Connection Details' : 'Edit Connection' }}
</h2>
<form class="edit-connection-form" [formGroup]="editConnectionForm">
<mat-dialog-content>
<mat-tab-group>
@ -42,9 +44,11 @@
formControlName="source"></source-remote-process-group>
}
@case (ComponentType.InputPort) {
<source-input-port
[groupName]="getCurrentGroupName(breadcrumbs)"
[inputPortName]="source.name"></source-input-port>
@if (sourceInputPort$ | async) {
<source-input-port
[groupName]="getCurrentGroupName(breadcrumbs)"
[inputPortName]="source.name"></source-input-port>
}
}
@case (ComponentType.Funnel) {
<source-funnel [groupName]="getCurrentGroupName(breadcrumbs)"></source-funnel>
@ -70,9 +74,11 @@
formControlName="destination"></destination-remote-process-group>
}
@case (ComponentType.OutputPort) {
<destination-output-port
[groupName]="getCurrentGroupName(breadcrumbs)"
[outputPortName]="destinationName"></destination-output-port>
@if (destinationOutputPort$ | async) {
<destination-output-port
[groupName]="getCurrentGroupName(breadcrumbs)"
[outputPortName]="destinationName"></destination-output-port>
}
}
@case (ComponentType.Funnel) {
<destination-funnel
@ -89,7 +95,11 @@
<div>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput formControlName="name" type="text" />
<input
matInput
formControlName="name"
type="text"
[readonly]="connectionReadonly || sourceReadonly || destinationReadonly" />
</mat-form-field>
</div>
<div class="flex flex-col mb-5">
@ -99,20 +109,32 @@
<div>
<mat-form-field>
<mat-label>FlowFile Expiration</mat-label>
<input matInput formControlName="flowFileExpiration" type="text" />
<input
matInput
formControlName="flowFileExpiration"
type="text"
[readonly]="connectionReadonly || sourceReadonly || destinationReadonly" />
</mat-form-field>
</div>
<div class="flex gap-x-2">
<div class="w-full">
<mat-form-field>
<mat-label>Back Pressure Object Threshold</mat-label>
<input matInput formControlName="backPressureObjectThreshold" type="number" />
<input
matInput
formControlName="backPressureObjectThreshold"
type="number"
[readonly]="connectionReadonly || sourceReadonly || destinationReadonly" />
</mat-form-field>
</div>
<div class="w-full">
<mat-form-field>
<mat-label>Size Threshold</mat-label>
<input matInput formControlName="backPressureDataSizeThreshold" type="text" />
<input
matInput
formControlName="backPressureDataSizeThreshold"
type="text"
[readonly]="connectionReadonly || sourceReadonly || destinationReadonly" />
</mat-form-field>
</div>
</div>
@ -126,6 +148,7 @@
@for (option of loadBalanceStrategies; track option) {
<mat-option
[value]="option.value"
[disabled]="connectionReadonly || sourceReadonly || destinationReadonly"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
@ -140,7 +163,11 @@
<div class="w-full">
<mat-form-field>
<mat-label>Attribute Name</mat-label>
<input matInput formControlName="partitionAttribute" type="text" />
<input
matInput
formControlName="partitionAttribute"
type="text"
[readonly]="connectionReadonly || sourceReadonly || destinationReadonly" />
</mat-form-field>
</div>
}
@ -153,6 +180,7 @@
@for (option of loadBalanceCompressionStrategies; track option) {
<mat-option
[value]="option.value"
[disabled]="connectionReadonly || sourceReadonly || destinationReadonly"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
@ -176,15 +204,19 @@
</mat-dialog-content>
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close="CANCELLED">Cancel</button>
<button
[disabled]="!editConnectionForm.dirty || editConnectionForm.invalid || saving.value"
type="button"
color="primary"
(click)="editConnection()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
@if (connectionReadonly || sourceReadonly || destinationReadonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close="CANCELLED">Cancel</button>
<button
[disabled]="!editConnectionForm.dirty || editConnectionForm.invalid || saving.value"
type="button"
color="primary"
(click)="editConnection()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
}
</mat-dialog-actions>
}
</form>

View File

@ -49,7 +49,7 @@ import { SourceFunnel } from '../source/source-funnel/source-funnel.component';
import { DestinationProcessor } from '../destination/destination-processor/destination-processor.component';
import { DestinationOutputPort } from '../destination/destination-output-port/destination-output-port.component';
import { SourceInputPort } from '../source/source-input-port/source-input-port.component';
import { Observable } from 'rxjs';
import { asyncScheduler, Observable, observeOn, tap } from 'rxjs';
import { SourceProcessGroup } from '../source/source-process-group/source-process-group.component';
import { DestinationProcessGroup } from '../destination/destination-process-group/destination-process-group.component';
import { SourceRemoteProcessGroup } from '../source/source-remote-process-group/source-remote-process-group.component';
@ -92,39 +92,96 @@ export class EditConnectionComponent {
@Input() set getChildOutputPorts(getChildOutputPorts: (groupId: string) => Observable<any>) {
if (this.sourceType == ComponentType.ProcessGroup) {
this.childOutputPorts$ = getChildOutputPorts(this.source.groupId);
this.sourceReadonly = false;
this.updateControlValueAccessorsForReadOnly();
}
}
@Input() set getChildInputPorts(getChildInputPorts: (groupId: string) => Observable<any>) {
if (this.destinationType == ComponentType.ProcessGroup) {
this.childInputPorts$ = getChildInputPorts(this.destinationGroupId);
this.destinationReadonly = false;
this.updateControlValueAccessorsForReadOnly();
}
}
@Input() set selectProcessor(selectProcessor: (id: string) => Observable<any>) {
if (this.sourceType == ComponentType.Processor) {
this.sourceProcessor$ = selectProcessor(this.source.id);
this.sourceProcessor$ = selectProcessor(this.source.id).pipe(
observeOn(asyncScheduler),
tap((processor) => {
this.sourceReadonly = !this.canvasUtils.runnableSupportsModification(processor);
this.updateControlValueAccessorsForReadOnly();
})
);
}
if (this.destinationType == ComponentType.Processor && this.destinationId) {
this.destinationProcessor$ = selectProcessor(this.destinationId);
this.destinationProcessor$ = selectProcessor(this.destinationId).pipe(
observeOn(asyncScheduler),
tap((processor) => {
this.destinationReadonly = !this.canvasUtils.runnableSupportsModification(processor);
this.updateControlValueAccessorsForReadOnly();
})
);
}
}
@Input() set selectInputPort(selectInputPort: (id: string) => Observable<any>) {
if (this.sourceType == ComponentType.InputPort) {
this.sourceInputPort$ = selectInputPort(this.source.id).pipe(
observeOn(asyncScheduler),
tap((inputPort) => {
this.sourceReadonly = !this.canvasUtils.runnableSupportsModification(inputPort);
this.updateControlValueAccessorsForReadOnly();
})
);
}
}
@Input() set selectOutputPort(selectOutputPort: (id: string) => Observable<any>) {
if (this.destinationType == ComponentType.OutputPort && this.destinationId) {
this.destinationOutputPort$ = selectOutputPort(this.destinationId).pipe(
observeOn(asyncScheduler),
tap((outputPort) => {
this.destinationReadonly = !this.canvasUtils.runnableSupportsModification(outputPort);
this.updateControlValueAccessorsForReadOnly();
})
);
}
}
@Input() set selectProcessGroup(selectProcessGroup: (id: string) => Observable<any>) {
if (this.sourceType == ComponentType.ProcessGroup) {
this.sourceProcessGroup$ = selectProcessGroup(this.source.groupId);
this.sourceReadonly = false;
this.updateControlValueAccessorsForReadOnly();
}
if (this.destinationType == ComponentType.ProcessGroup) {
this.destinationProcessGroup$ = selectProcessGroup(this.destinationGroupId);
this.destinationReadonly = false;
this.updateControlValueAccessorsForReadOnly();
}
}
@Input() set selectRemoteProcessGroup(selectRemoteProcessGroup: (id: string) => Observable<any>) {
if (this.sourceType == ComponentType.RemoteProcessGroup) {
this.sourceRemoteProcessGroup$ = selectRemoteProcessGroup(this.source.groupId);
this.sourceRemoteProcessGroup$ = selectRemoteProcessGroup(this.source.groupId).pipe(
observeOn(asyncScheduler),
tap((remoteProcessGroup) => {
this.sourceReadonly = !this.canvasUtils.remoteProcessGroupSupportsModification(remoteProcessGroup);
this.updateControlValueAccessorsForReadOnly();
})
);
}
if (this.destinationType == ComponentType.RemoteProcessGroup) {
this.destinationRemoteProcessGroup$ = selectRemoteProcessGroup(this.destinationGroupId);
this.destinationRemoteProcessGroup$ = selectRemoteProcessGroup(this.destinationGroupId).pipe(
observeOn(asyncScheduler),
tap((remoteProcessGroup) => {
this.destinationReadonly =
!this.canvasUtils.remoteProcessGroupSupportsModification(remoteProcessGroup);
this.updateControlValueAccessorsForReadOnly();
})
);
}
}
@ -136,6 +193,9 @@ export class EditConnectionComponent {
breadcrumbs$ = this.store.select(selectBreadcrumbs);
editConnectionForm: FormGroup;
connectionReadonly: boolean;
sourceReadonly: boolean = false;
destinationReadonly: boolean = false;
source: any;
sourceType: ComponentType | null;
@ -147,14 +207,16 @@ export class EditConnectionComponent {
destinationGroupId: string;
destinationName: string;
sourceProcessor$!: Observable<any> | null;
destinationProcessor$!: Observable<any> | null;
sourceProcessGroup$!: Observable<any> | null;
destinationProcessGroup$!: Observable<any> | null;
sourceRemoteProcessGroup$!: Observable<any> | null;
destinationRemoteProcessGroup$!: Observable<any> | null;
childOutputPorts$!: Observable<any> | null;
childInputPorts$!: Observable<any> | null;
sourceProcessor$: Observable<any> | null = null;
destinationProcessor$: Observable<any> | null = null;
sourceInputPort$: Observable<any> | null = null;
destinationOutputPort$: Observable<any> | null = null;
sourceProcessGroup$: Observable<any> | null = null;
destinationProcessGroup$: Observable<any> | null = null;
sourceRemoteProcessGroup$: Observable<any> | null = null;
destinationRemoteProcessGroup$: Observable<any> | null = null;
childOutputPorts$: Observable<any> | null = null;
childInputPorts$: Observable<any> | null = null;
loadBalancePartitionAttributeRequired = false;
initialPartitionAttribute: string;
@ -170,6 +232,8 @@ export class EditConnectionComponent {
) {
const connection: any = dialogRequest.entity.component;
this.connectionReadonly = !dialogRequest.entity.permissions.canWrite;
this.source = connection.source;
this.sourceType = this.canvasUtils.getComponentTypeForSource(this.source.type);
@ -228,6 +292,44 @@ export class EditConnectionComponent {
this.initialPartitionAttribute = connection.loadBalancePartitionAttribute;
this.initialCompression = connection.loadBalanceCompression;
this.loadBalanceChanged(connection.loadBalanceStrategy);
this.updateControlValueAccessorsForReadOnly();
}
updateControlValueAccessorsForReadOnly(): void {
const disabled = this.connectionReadonly || this.sourceReadonly || this.destinationReadonly;
// sourceReadonly is used to update the readonly / disable state of the form controls, note that
// the source control for local and remote groups is always disabled (see above) in this edit
// component because the source of the connection cannot be changed
if (disabled) {
this.editConnectionForm.get('prioritizers')?.disable();
if (this.sourceType == ComponentType.Processor) {
this.editConnectionForm.get('relationships')?.disable();
}
if (
this.destinationType == ComponentType.ProcessGroup ||
this.destinationType == ComponentType.RemoteProcessGroup
) {
this.editConnectionForm.get('destination')?.disable();
}
} else {
this.editConnectionForm.get('prioritizers')?.enable();
if (this.sourceType == ComponentType.Processor) {
this.editConnectionForm.get('relationships')?.enable();
}
if (
this.destinationType == ComponentType.ProcessGroup ||
this.destinationType == ComponentType.RemoteProcessGroup
) {
this.editConnectionForm.get('destination')?.enable();
}
}
}
getCurrentGroupName(breadcrumbs: BreadcrumbEntity): string {

View File

@ -21,6 +21,7 @@
border-radius: 4px;
overflow: hidden;
display: block;
user-select: none;
}
.prioritizer-draggable-item {

View File

@ -29,11 +29,12 @@
} @else {
<mat-form-field>
<mat-label>From Output</mat-label>
<mat-select [(ngModel)]="selectedOutputPort" (selectionChange)="handleChanged()" [disabled]="isDisabled">
<mat-select [(ngModel)]="selectedOutputPort" (selectionChange)="handleChanged()">
@for (item of outputPortItems; track item) {
@if (item.description) {
<mat-option
[value]="item.value"
[disabled]="isDisabled"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(item)"
@ -41,7 +42,7 @@
>{{ item.text }}</mat-option
>
} @else {
<mat-option [value]="item.value">{{ item.text }}</mat-option>
<mat-option [value]="item.value" [disabled]="isDisabled">{{ item.text }}</mat-option>
}
}
</mat-select>

View File

@ -26,12 +26,12 @@
} @else {
<mat-form-field>
<mat-label>From Output</mat-label>
<mat-select [(ngModel)]="selectedOutputPort" (selectionChange)="handleChanged()" [disabled]="isDisabled">
<mat-select [(ngModel)]="selectedOutputPort" (selectionChange)="handleChanged()">
@for (item of outputPortItems; track item) {
@if (item.description) {
<mat-option
[value]="item.value"
[disabled]="item.disabled == true"
[disabled]="item.disabled == true || isDisabled"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(item)"
@ -39,7 +39,9 @@
>{{ item.text }}</mat-option
>
} @else {
<mat-option [value]="item.value" [disabled]="item.disabled == true">{{ item.text }}</mat-option>
<mat-option [value]="item.value" [disabled]="item.disabled == true || isDisabled">{{
item.text
}}</mat-option>
}
}
</mat-select>

View File

@ -15,21 +15,21 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Edit {{ portTypeLabel }}</h2>
<h2 mat-dialog-title>{{ readonly ? portTypeLabel + ' Details' : 'Edit ' + portTypeLabel }}</h2>
<form class="edit-port-form" [formGroup]="editPortForm">
<error-banner></error-banner>
<mat-dialog-content>
<div>
<mat-form-field>
<mat-label>{{ portTypeLabel }} Name</mat-label>
<input matInput formControlName="name" type="text" />
<input matInput formControlName="name" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
<div>
@if (request.entity.component.allowRemoteAccess) {
<mat-form-field>
<mat-label>Concurrent Tasks</mat-label>
<input matInput formControlName="concurrentTasks" type="text" />
<input matInput formControlName="concurrentTasks" type="text" [readonly]="readonly" />
</mat-form-field>
}
</div>
@ -42,21 +42,25 @@
<div>
<mat-form-field>
<mat-label>Comments</mat-label>
<textarea matInput formControlName="comments" type="text"></textarea>
<textarea matInput formControlName="comments" type="text" [readonly]="readonly"></textarea>
</mat-form-field>
</div>
</mat-dialog-content>
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editPortForm.dirty || editPortForm.invalid || saving.value"
type="button"
color="primary"
(click)="editPort()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
@if (readonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editPortForm.dirty || editPortForm.invalid || saving.value"
type="button"
color="primary"
(click)="editPort()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
}
</mat-dialog-actions>
}
</form>

View File

@ -32,6 +32,7 @@ import { AsyncPipe } from '@angular/common';
import { selectSaving } from '../../../../../state/flow/flow.selectors';
import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nifi-spinner.directive';
import { ClusterConnectionService } from '../../../../../../../service/cluster-connection.service';
import { CanvasUtils } from '../../../../../service/canvas-utils.service';
@Component({
selector: 'edit-port',
@ -53,15 +54,20 @@ export class EditPort {
saving$ = this.store.select(selectSaving);
editPortForm: FormGroup;
readonly: boolean;
portTypeLabel: string;
constructor(
@Inject(MAT_DIALOG_DATA) public request: EditComponentDialogRequest,
private formBuilder: FormBuilder,
private store: Store<CanvasState>,
private canvasUtils: CanvasUtils,
private client: Client,
private clusterConnectionService: ClusterConnectionService
) {
this.readonly =
!request.entity.permissions.canWrite || !this.canvasUtils.runnableSupportsModification(request.entity);
// set the port type name
if (ComponentType.InputPort == this.request.type) {
this.portTypeLabel = 'Input Port';
@ -77,7 +83,10 @@ export class EditPort {
Validators.required
),
comments: new FormControl(request.entity.component.comments),
portFunction: new FormControl(request.entity.component.portFunction == 'FAILURE')
portFunction: new FormControl({
value: request.entity.component.portFunction == 'FAILURE',
disabled: this.readonly
})
});
}
@ -98,7 +107,7 @@ export class EditPort {
}
// if this port is an output port update the port function
if (ComponentType.OutputPort == this.request.entity.type) {
if (ComponentType.OutputPort == this.request.type) {
payload.component.portFunction = this.editPortForm.get('portFunction')?.value ? 'FAILURE' : 'STANDARD';
}

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Edit Process Group</h2>
<h2 mat-dialog-title>{{ readonly ? 'Process Group Details' : 'Edit Process Group' }}</h2>
<form class="process-group-edit-form" [formGroup]="editProcessGroupForm">
<mat-dialog-content>
<mat-tab-group>
@ -25,7 +25,7 @@
<div>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput formControlName="name" type="text" />
<input matInput formControlName="name" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
<div>
@ -36,6 +36,7 @@
@if (option.description) {
<mat-option
[value]="option.value"
[disabled]="readonly"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
@ -43,7 +44,9 @@
>{{ option.text }}</mat-option
>
} @else {
<mat-option [value]="option.value">{{ option.text }}</mat-option>
<mat-option [value]="option.value" [disabled]="readonly">{{
option.text
}}</mat-option>
}
}
</mat-select>
@ -64,6 +67,7 @@
@for (option of executionEngineOptions; track option) {
<mat-option
[value]="option.value"
[disabled]="readonly"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
@ -81,6 +85,7 @@
@for (option of flowfileConcurrencyOptions; track option) {
<mat-option
[value]="option.value"
[disabled]="readonly"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
@ -98,6 +103,7 @@
@for (option of flowfileOutboundPolicyOptions; track option) {
<mat-option
[value]="option.value"
[disabled]="readonly"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
@ -113,25 +119,37 @@
<div>
<mat-form-field>
<mat-label>Default FlowFile Expiration</mat-label>
<input matInput formControlName="defaultFlowFileExpiration" type="text" />
<input
matInput
formControlName="defaultFlowFileExpiration"
type="text"
[readonly]="readonly" />
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>Default Back Pressure Object Threshold</mat-label>
<input matInput formControlName="defaultBackPressureObjectThreshold" type="text" />
<input
matInput
formControlName="defaultBackPressureObjectThreshold"
type="text"
[readonly]="readonly" />
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>Default Back Pressure Data Size Threshold</mat-label>
<input matInput formControlName="defaultBackPressureDataSizeThreshold" type="text" />
<input
matInput
formControlName="defaultBackPressureDataSizeThreshold"
type="text"
[readonly]="readonly" />
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>Log File Suffix</mat-label>
<input matInput formControlName="logFileSuffix" type="text" />
<input matInput formControlName="logFileSuffix" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
</div>
@ -141,7 +159,12 @@
<div class="tab-content py-4">
<mat-form-field>
<mat-label>Comments</mat-label>
<textarea matInput formControlName="comments" type="text" rows="15"></textarea>
<textarea
matInput
formControlName="comments"
type="text"
rows="15"
[readonly]="readonly"></textarea>
</mat-form-field>
</div>
</mat-tab>
@ -149,16 +172,20 @@
</mat-dialog-content>
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editProcessGroupForm.dirty || editProcessGroupForm.invalid || saving.value"
class="h-8"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
@if (readonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editProcessGroupForm.dirty || editProcessGroupForm.invalid || saving.value"
class="h-8"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
}
</mat-dialog-actions>
}
</form>

View File

@ -86,6 +86,7 @@ export class EditProcessGroup {
protected readonly TextTip = TextTip;
editProcessGroupForm: FormGroup;
readonly: boolean;
parameterContextsOptions: SelectOption[] = [];
executionEngineOptions: SelectOption[] = [
@ -155,6 +156,8 @@ export class EditProcessGroup {
private client: Client,
private clusterConnectionService: ClusterConnectionService
) {
this.readonly = !request.entity.permissions.canWrite;
this.parameterContextsOptions.push({
text: 'No parameter context',
value: null
@ -162,7 +165,7 @@ export class EditProcessGroup {
this.editProcessGroupForm = this.formBuilder.group({
name: new FormControl(request.entity.component.name, Validators.required),
applyParameterContextRecursively: new FormControl(false),
applyParameterContextRecursively: new FormControl({ value: false, disabled: this.readonly }),
executionEngine: new FormControl(request.entity.component.executionEngine, Validators.required),
flowfileConcurrency: new FormControl(request.entity.component.flowfileConcurrency, Validators.required),
flowfileOutboundPolicy: new FormControl(

View File

@ -15,7 +15,16 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Edit Processor</h2>
<h2 mat-dialog-title>
<div class="flex justify-between items-baseline">
<div>
{{ readonly ? 'Processor Details' : 'Edit Processor' }}
</div>
<div class="text-sm">
{{ formatType(request.entity) }}
</div>
</div>
</h2>
<form class="processor-edit-form" [formGroup]="editProcessorForm">
<error-banner></error-banner>
<!-- TODO - Stop & Configure -->
@ -27,7 +36,7 @@
<div>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput formControlName="name" type="text" />
<input matInput formControlName="name" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
<div class="flex flex-col mb-5">
@ -50,13 +59,17 @@
<div class="w-full">
<mat-form-field>
<mat-label>Penalty Duration</mat-label>
<input matInput formControlName="penaltyDuration" type="text" />
<input
matInput
formControlName="penaltyDuration"
type="text"
[readonly]="readonly" />
</mat-form-field>
</div>
<div class="w-full">
<mat-form-field>
<mat-label>Yield Duration</mat-label>
<input matInput formControlName="yieldDuration" type="text" />
<input matInput formControlName="yieldDuration" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
</div>
@ -65,7 +78,7 @@
<mat-label>Bulletin Level</mat-label>
<mat-select formControlName="bulletinLevel">
@for (option of bulletinLevels; track option) {
<mat-option [value]="option.value">
<mat-option [value]="option.value" [disabled]="readonly">
{{ option.text }}
</mat-option>
}
@ -88,6 +101,7 @@
@for (option of schedulingStrategies; track option) {
<mat-option
[value]="option.value"
[disabled]="readonly"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getSelectOptionTipData(option)"
@ -107,7 +121,8 @@
formControlName="concurrentTasks"
(change)="concurrentTasksChanged()"
name="concurrentTasks"
type="number" />
type="number"
[readonly]="readonly" />
</mat-form-field>
</div>
<div class="w-44">
@ -117,7 +132,8 @@
matInput
formControlName="schedulingPeriod"
(change)="schedulingPeriodChanged()"
type="text" />
type="text"
[readonly]="readonly" />
</mat-form-field>
</div>
</div>
@ -127,7 +143,7 @@
<mat-select formControlName="executionNode">
@for (option of executionStrategies; track option) {
<mat-option
[disabled]="executionStrategyDisabled(option)"
[disabled]="executionStrategyDisabled(option) || readonly"
[value]="option.value"
nifiTooltip
[tooltipComponentType]="TextTip"
@ -189,7 +205,12 @@
<div class="tab-content py-4">
<mat-form-field>
<mat-label>Comments</mat-label>
<textarea matInput formControlName="comments" type="text" rows="5"></textarea>
<textarea
matInput
formControlName="comments"
type="text"
rows="5"
[readonly]="readonly"></textarea>
</mat-form-field>
</div>
</mat-tab>
@ -197,15 +218,19 @@
</mat-dialog-content>
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editProcessorForm.dirty || editProcessorForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
@if (readonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editProcessorForm.dirty || editProcessorForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
}
</mat-dialog-actions>
}
</form>

View File

@ -17,6 +17,10 @@
@use '@angular/material' as mat;
h2.mdc-dialog__title::before {
height: 0;
}
.processor-edit-form {
@include mat.button-density(-1);

View File

@ -49,6 +49,7 @@ import {
} from './relationship-settings/relationship-settings.component';
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
import { ClusterConnectionService } from '../../../../../../../service/cluster-connection.service';
import { CanvasUtils } from '../../../../../service/canvas-utils.service';
@Component({
selector: 'edit-processor',
@ -87,6 +88,7 @@ export class EditProcessor {
protected readonly TextTip = TextTip;
editProcessorForm: FormGroup;
readonly: boolean;
bulletinLevels = [
{
@ -148,9 +150,13 @@ export class EditProcessor {
@Inject(MAT_DIALOG_DATA) public request: EditComponentDialogRequest,
private formBuilder: FormBuilder,
private client: Client,
private canvasUtils: CanvasUtils,
private clusterConnectionService: ClusterConnectionService,
private nifiCommon: NiFiCommon
) {
this.readonly =
!request.entity.permissions.canWrite || !this.canvasUtils.runnableSupportsModification(request.entity);
const processorProperties: any = request.entity.component.config.properties;
const properties: Property[] = Object.entries(processorProperties).map((entry: any) => {
const [property, value] = entry;
@ -205,15 +211,18 @@ export class EditProcessor {
concurrentTasks: new FormControl(concurrentTasks, Validators.required),
schedulingPeriod: new FormControl(schedulingPeriod, Validators.required),
executionNode: new FormControl(request.entity.component.config.executionNode, Validators.required),
properties: new FormControl(properties),
relationshipConfiguration: new FormControl(relationshipConfiguration, Validators.required),
properties: new FormControl({ value: properties, disabled: this.readonly }),
relationshipConfiguration: new FormControl(
{ value: relationshipConfiguration, disabled: this.readonly },
Validators.required
),
comments: new FormControl(request.entity.component.config.comments)
});
if (this.supportsBatching()) {
this.editProcessorForm.addControl(
'runDuration',
new FormControl(this.runDurationMillis, Validators.required)
new FormControl({ value: this.runDurationMillis, disabled: this.readonly }, Validators.required)
);
}
}

View File

@ -72,7 +72,7 @@
type="number"
name="retryCount"
(keyup)="handleChanged()"
[disabled]="isDisabled" />
[readonly]="isDisabled" />
<mat-icon
matSuffix
class="info-icon"
@ -107,7 +107,7 @@
[(ngModel)]="maxBackoffPeriod"
type="text"
(keyup)="handleChanged()"
[disabled]="isDisabled" />
[readonly]="isDisabled" />
<mat-icon
matSuffix
class="info-icon"

View File

@ -16,20 +16,17 @@
-->
<div class="run-duration-slider flex flex-col">
<div class="pl-4 pr-7">
<mat-slider
[min]="0"
[max]="7"
[step]="1"
showTickMarks
discrete
[displayWith]="formatLabel"
[disabled]="isDisabled">
<input matSliderThumb (valueChange)="runDurationChanged($event)" />
</mat-slider>
</div>
<div class="flex justify-between items-center">
<div class="accent-color font-medium">Lower latency</div>
<div class="accent-color font-medium">Higher throughput</div>
</div>
@if (isDisabled) {
<div class="accent-color font-medium">{{ formatLabel(runDurationIndex) }}</div>
} @else {
<div class="pl-4 pr-7">
<mat-slider [min]="0" [max]="7" [step]="1" showTickMarks discrete [displayWith]="formatLabel">
<input matSliderThumb [value]="runDurationIndex" (valueChange)="runDurationChanged($event)" />
</mat-slider>
</div>
<div class="flex justify-between items-center">
<div class="accent-color font-medium">Lower latency</div>
<div class="accent-color font-medium">Higher throughput</div>
</div>
}
</div>

View File

@ -79,7 +79,7 @@ export class RunDurationSlider implements ControlValueAccessor {
if (index < 0) {
this.runDurationIndex = 0;
} else {
this.runDurationIndex = runDuration;
this.runDurationIndex = index;
}
}

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Edit Remote Process Group</h2>
<h2 mat-dialog-title>{{ readonly ? 'Remote Process Group Details' : 'Edit Remote Process Group' }}</h2>
<form class="edit-remote-process-group-form" [formGroup]="editRemoteProcessGroupForm">
<error-banner></error-banner>
<mat-dialog-content>
@ -31,7 +31,12 @@
<div class="w-full">
<mat-form-field>
<mat-label>URLs</mat-label>
<input matInput formControlName="urls" type="text" placeholder="https://remotehost:8443/nifi" />
<input
matInput
formControlName="urls"
type="text"
placeholder="https://remotehost:8443/nifi"
[readonly]="readonly" />
</mat-form-field>
</div>
</div>
@ -40,15 +45,15 @@
<mat-form-field>
<mat-label>Transport Protocol</mat-label>
<mat-select formControlName="transportProtocol">
<mat-option value="RAW"> RAW</mat-option>
<mat-option value="HTTP"> HTTP</mat-option>
<mat-option value="RAW" [disabled]="readonly"> RAW</mat-option>
<mat-option value="HTTP" [disabled]="readonly"> HTTP</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="w-full">
<mat-form-field>
<mat-label>Local Network Interface</mat-label>
<input matInput formControlName="localNetworkInterface" type="text" />
<input matInput formControlName="localNetworkInterface" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
</div>
@ -56,13 +61,13 @@
<div class="w-full">
<mat-form-field>
<mat-label>HTTP Proxy Server Hostname</mat-label>
<input matInput formControlName="httpProxyServerHostname" type="text" />
<input matInput formControlName="httpProxyServerHostname" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
<div class="w-full">
<mat-form-field>
<mat-label>HTTP Proxy Server Port</mat-label>
<input matInput formControlName="httpProxyServerPort" type="text" />
<input matInput formControlName="httpProxyServerPort" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
</div>
@ -70,13 +75,13 @@
<div class="w-full">
<mat-form-field>
<mat-label>HTTP Proxy User</mat-label>
<input matInput formControlName="httpProxyUser" type="text" />
<input matInput formControlName="httpProxyUser" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
<div class="w-full">
<mat-form-field>
<mat-label>HTTP Proxy Password</mat-label>
<input matInput formControlName="httpProxyPassword" type="text" />
<input matInput formControlName="httpProxyPassword" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
</div>
@ -84,28 +89,32 @@
<div class="w-full">
<mat-form-field>
<mat-label>Communications Timeout</mat-label>
<input matInput formControlName="communicationsTimeout" type="text" />
<input matInput formControlName="communicationsTimeout" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
<div class="w-full">
<mat-form-field>
<mat-label>Yield Duration</mat-label>
<input matInput formControlName="yieldDuration" type="text" />
<input matInput formControlName="yieldDuration" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
</div>
</mat-dialog-content>
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editRemoteProcessGroupForm.dirty || editRemoteProcessGroupForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Add</span>
</button>
@if (readonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editRemoteProcessGroupForm.dirty || editRemoteProcessGroupForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Add</span>
</button>
}
</mat-dialog-actions>
}
</form>

View File

@ -29,16 +29,21 @@ describe('EditRemoteProcessGroup', () => {
let fixture: ComponentFixture<EditRemoteProcessGroup>;
const data: any = {
revision: {
clientId: 'a6482293-7fe8-43b4-8ab4-ee95b3b27721',
version: 0
},
type: ComponentType.RemoteProcessGroup,
position: {
x: -4,
y: -698.5
},
uri: 'https://localhost:4200/nifi-api/remote-process-groups/abd5a02c-018b-1000-c602-fe83979f1997',
entity: {
revision: {
clientId: 'a6482293-7fe8-43b4-8ab4-ee95b3b27721',
version: 0
},
position: {
x: -4,
y: -698.5
},
permissions: {
canRead: true,
canWrite: false
},
component: {
activeRemoteInputPortCount: 0,
activeRemoteOutputPortCount: 0,

View File

@ -30,6 +30,7 @@ import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nif
import { TextTip } from '../../../../../../../ui/common/tooltips/text-tip/text-tip.component';
import { EditComponentDialogRequest } from '../../../../../state/flow';
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
import { CanvasUtils } from '../../../../../service/canvas-utils.service';
@Component({
standalone: true,
@ -56,12 +57,18 @@ export class EditRemoteProcessGroup {
protected readonly TextTip = TextTip;
editRemoteProcessGroupForm: FormGroup;
readonly: boolean;
constructor(
@Inject(MAT_DIALOG_DATA) public request: EditComponentDialogRequest,
private formBuilder: FormBuilder,
private canvasUtils: CanvasUtils,
private client: Client
) {
this.readonly =
!request.entity.permissions.canWrite ||
!this.canvasUtils.remoteProcessGroupSupportsModification(request.entity);
this.editRemoteProcessGroupForm = this.formBuilder.group({
urls: new FormControl(request.entity.component.targetUris, Validators.required),
transportProtocol: new FormControl(request.entity.component.transportProtocol, Validators.required),

View File

@ -193,7 +193,7 @@
port.transmitting === false
) {
<div
class="pointer fa fa-pencil primary-color"
class="pointer fa fa-cog primary-color"
(click)="configureClicked(port, $event)"
title="Edit Port"></div>
}

View File

@ -15,7 +15,9 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>{{ this.isNew ? 'Add' : 'Edit' }} Parameter Context</h2>
<h2 mat-dialog-title>
{{ readonly ? 'Parameter Context Details' : isNew ? 'Add Parameter Context' : 'Edit Parameter Context' }}
</h2>
<form class="parameter-context-edit-form" [formGroup]="editParameterContextForm">
<error-banner></error-banner>
@if ((updateRequest | async)!; as requestEntity) {
@ -101,13 +103,18 @@
<div>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput formControlName="name" type="text" />
<input matInput formControlName="name" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>Description</mat-label>
<textarea matInput formControlName="description" type="text" rows="5"></textarea>
<textarea
matInput
formControlName="description"
type="text"
rows="5"
[readonly]="readonly"></textarea>
</mat-form-field>
</div>
</div>
@ -156,15 +163,19 @@
} @else {
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editParameterContextForm.dirty || editParameterContextForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
@if (readonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editParameterContextForm.dirty || editParameterContextForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
}
</mat-dialog-actions>
}
}

View File

@ -79,6 +79,8 @@ export class EditParameterContext {
@Output() editParameterContext: EventEmitter<any> = new EventEmitter<any>();
editParameterContextForm: FormGroup;
readonly: boolean;
isNew: boolean;
parameterProvider: ParameterProviderConfiguration | null = null;
@ -92,20 +94,26 @@ export class EditParameterContext {
) {
if (request.parameterContext) {
this.isNew = false;
this.readonly = !request.parameterContext.permissions.canWrite;
this.editParameterContextForm = this.formBuilder.group({
name: new FormControl(request.parameterContext.component.name, Validators.required),
description: new FormControl(request.parameterContext.component.description),
parameters: new FormControl(request.parameterContext.component.parameters),
inheritedParameterContexts: new FormControl(
request.parameterContext.component.inheritedParameterContexts
)
parameters: new FormControl({
value: request.parameterContext.component.parameters,
disabled: this.readonly
}),
inheritedParameterContexts: new FormControl({
value: request.parameterContext.component.inheritedParameterContexts,
disabled: this.readonly
})
});
if (request.parameterContext.component.parameterProviderConfiguration) {
this.parameterProvider = request.parameterContext.component.parameterProviderConfiguration.component;
}
} else {
this.isNew = true;
this.readonly = false;
this.editParameterContextForm = this.formBuilder.group({
name: new FormControl('', Validators.required),

View File

@ -25,21 +25,6 @@
(matSortChange)="sortData($event)"
[matSortActive]="initialSortColumn"
[matSortDirection]="initialSortDirection">
<!-- More Details Column -->
<ng-container matColumnDef="moreDetails">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
@if (canRead(item)) {
<div class="flex items-center gap-x-2">
<!-- TODO - handle read only in configure component? -->
@if (canRead(item)) {
<div class="pointer fa fa-info-circle primary-color"></div>
}
</div>
}
</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
@ -73,11 +58,11 @@
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center justify-end gap-x-2">
@if (canRead(item) && canWrite(item)) {
@if (canRead(item)) {
<div
class="pointer fa fa-pencil primary-color"
class="pointer fa fa-cog primary-color"
(click)="editClicked(item, $event)"
title="Edit"></div>
[title]="canWrite(item) ? 'Edit' : 'View Configuration'"></div>
}
@if (canDelete(item)) {
<div

View File

@ -17,10 +17,6 @@
.parameter-context-table {
.listing-table {
.mat-column-moreDetails {
width: 32px;
}
.mat-column-name {
width: 25%;
}

View File

@ -48,7 +48,7 @@ export class ParameterContextTable {
@Output() editParameterContext: EventEmitter<ParameterContextEntity> = new EventEmitter<ParameterContextEntity>();
@Output() deleteParameterContext: EventEmitter<ParameterContextEntity> = new EventEmitter<ParameterContextEntity>();
displayedColumns: string[] = ['moreDetails', 'name', 'provider', 'description', 'actions'];
displayedColumns: string[] = ['name', 'provider', 'description', 'actions'];
dataSource: MatTableDataSource<ParameterContextEntity> = new MatTableDataSource<ParameterContextEntity>();
constructor(private nifiCommon: NiFiCommon) {}

View File

@ -17,7 +17,7 @@
<div class="parameter-table flex gap-x-3">
<div class="flex basis-2/3 flex-col gap-y-3">
@if (canAddParameters) {
@if (canAddParameters && !isDisabled) {
<div class="flex justify-end items-center">
<button mat-icon-button color="primary" type="button" (click)="newParameterClicked()">
<i class="fa fa-plus"></i>
@ -101,13 +101,13 @@
mat-dialog-close="ROUTED"
title="Go to"></div>
}
@if (canEdit(item)) {
@if (canEdit(item) && !isDisabled) {
<div
class="pointer fa fa-pencil primary-color"
class="pointer fa fa-cog primary-color"
(click)="editClicked(item)"
title="Edit"></div>
}
@if (canDelete(item)) {
@if (canDelete(item) && !isDisabled) {
<div
class="pointer fa fa-trash primary-color"
(click)="deleteClicked(item)"

View File

@ -109,7 +109,6 @@ export class ParameterTable implements AfterViewInit, ControlValueAccessor {
}
setDisabledState(isDisabled: boolean): void {
// TODO - update component to disable controls accordingly
this.isDisabled = isDisabled;
}

View File

@ -15,7 +15,16 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Edit Flow Analysis Rule</h2>
<h2 mat-dialog-title>
<div class="flex justify-between items-baseline">
<div>
{{ readonly ? 'Flow Analysis Rule Details' : 'Edit Flow Analysis Rule' }}
</div>
<div class="text-sm">
{{ formatType(request.flowAnalysisRule) }}
</div>
</div>
</h2>
<form class="flow-analysis-rule-edit-form" [formGroup]="editFlowAnalysisRuleForm">
<error-banner></error-banner>
<mat-dialog-content>
@ -25,7 +34,7 @@
<div class="w-full">
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput formControlName="name" type="text" />
<input matInput formControlName="name" type="text" [readonly]="readonly" />
</mat-form-field>
<div class="flex flex-col mb-5">
<div>Id</div>
@ -54,6 +63,7 @@
@for (option of strategies; track option) {
<mat-option
[value]="option.value"
[disabled]="readonly"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getPropertyTipData(option)"
@ -82,7 +92,12 @@
<div class="tab-content py-4">
<mat-form-field>
<mat-label>Comments</mat-label>
<textarea matInput formControlName="comments" type="text" rows="5"></textarea>
<textarea
matInput
formControlName="comments"
type="text"
rows="5"
[readonly]="readonly"></textarea>
</mat-form-field>
</div>
</mat-tab>
@ -90,15 +105,19 @@
</mat-dialog-content>
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editFlowAnalysisRuleForm.dirty || editFlowAnalysisRuleForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
@if (readonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editFlowAnalysisRuleForm.dirty || editFlowAnalysisRuleForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
}
</mat-dialog-actions>
}
</form>

View File

@ -17,6 +17,10 @@
@use '@angular/material' as mat;
h2.mdc-dialog__title::before {
height: 0;
}
.flow-analysis-rule-edit-form {
@include mat.button-density(-1);

View File

@ -79,6 +79,7 @@ export class EditFlowAnalysisRule {
new EventEmitter<UpdateFlowAnalysisRuleRequest>();
editFlowAnalysisRuleForm: FormGroup;
readonly: boolean;
strategies: SelectOption[] = [
{
@ -95,6 +96,9 @@ export class EditFlowAnalysisRule {
private nifiCommon: NiFiCommon,
private clusterConnectionService: ClusterConnectionService
) {
this.readonly =
!request.flowAnalysisRule.permissions.canWrite || request.flowAnalysisRule.status.runStatus !== 'DISABLED';
const serviceProperties: any = request.flowAnalysisRule.component.properties;
const properties: Property[] = Object.entries(serviceProperties).map((entry: any) => {
const [property, value] = entry;
@ -110,7 +114,7 @@ export class EditFlowAnalysisRule {
name: new FormControl(request.flowAnalysisRule.component.name, Validators.required),
state: new FormControl(request.flowAnalysisRule.component.state === 'STOPPED', Validators.required),
enforcementPolicy: new FormControl('ENFORCE', Validators.required),
properties: new FormControl(properties),
properties: new FormControl({ value: properties, disabled: this.readonly }),
comments: new FormControl(request.flowAnalysisRule.component.comments)
});
}

View File

@ -35,7 +35,6 @@
class="pointer fa fa-book primary-color"
(click)="viewDocumentationClicked(item, $event)"
title="View Documentation"></div>
<!-- TODO - handle read only in configure component? -->
@if (hasComments(item)) {
<div
class="pointer fa fa-comment primary-color"
@ -116,13 +115,10 @@
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center justify-end gap-x-2">
@if (canConfigure(item)) {
<div
class="pointer fa fa-cog primary-color"
(click)="configureClicked(item, $event)"
title="Edit"></div>
}
<!-- TODO - handle read only in configure component? -->
<div
class="pointer fa fa-cog primary-color"
(click)="configureClicked(item, $event)"
[title]="canConfigure(item) ? 'Edit' : 'View Configuration'"></div>
@if (canDisable(item)) {
<div
class="pointer fa icon icon-enable-false primary-color"

View File

@ -15,7 +15,14 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Edit Parameter Provider</h2>
<h2 mat-dialog-title>
<div class="flex justify-between items-baseline">
<div>Edit Parameter Provider</div>
<div class="text-sm">
{{ formatType(request.parameterProvider) }}
</div>
</div>
</h2>
<form class="parameter-provider-edit-form" [formGroup]="editParameterProviderForm">
<error-banner></error-banner>
<mat-dialog-content>
@ -26,7 +33,7 @@
<div>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput formControlName="name" type="text" />
<input matInput formControlName="name" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
<div class="flex flex-col mb-5">
@ -80,7 +87,12 @@
<div class="tab-content py-4">
<mat-form-field>
<mat-label>Comments</mat-label>
<textarea matInput formControlName="comments" type="text" rows="5"></textarea>
<textarea
matInput
formControlName="comments"
type="text"
rows="5"
[readonly]="readonly"></textarea>
</mat-form-field>
</div>
</mat-tab>
@ -89,15 +101,19 @@
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editParameterProviderForm.dirty || editParameterProviderForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
@if (readonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editParameterProviderForm.dirty || editParameterProviderForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
}
</mat-dialog-actions>
}
</form>

View File

@ -17,6 +17,10 @@
@use '@angular/material' as mat;
h2.mdc-dialog__title::before {
height: 0;
}
.parameter-provider-edit-form {
@include mat.button-density(-1);

View File

@ -76,6 +76,7 @@ export class EditParameterProvider {
new EventEmitter<UpdateParameterProviderRequest>();
editParameterProviderForm: FormGroup;
readonly: boolean;
constructor(
@Inject(MAT_DIALOG_DATA) public request: EditParameterProviderRequest,
@ -84,6 +85,8 @@ export class EditParameterProvider {
private nifiCommon: NiFiCommon,
private clusterConnectionService: ClusterConnectionService
) {
this.readonly = !request.parameterProvider.permissions.canWrite;
const providerProperties = request.parameterProvider.component.properties;
const properties: Property[] = Object.entries(providerProperties).map((entry: any) => {
const [property, value] = entry;
@ -97,7 +100,7 @@ export class EditParameterProvider {
// build the form
this.editParameterProviderForm = this.formBuilder.group({
name: new FormControl(request.parameterProvider.component.name, Validators.required),
properties: new FormControl(properties),
properties: new FormControl({ value: properties, disabled: this.readonly }),
comments: new FormControl(request.parameterProvider.component.comments)
});
}

View File

@ -33,9 +33,6 @@
<td mat-cell *matCellDef="let item">
@if (canRead(item)) {
<div class="flex items-center gap-x-2">
<!-- TODO: open details -->
<div class="pointer fa fa-info-circle primary-color" title="View details"></div>
<div
class="pointer fa fa-book primary-color"
(click)="viewDocumentationClicked(item, $event)"
@ -96,11 +93,11 @@
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center justify-end gap-x-2">
@if (canConfigure(item)) {
@if (canRead(item)) {
<div
class="pointer fa fa-cog primary-color"
(click)="configureClicked(item, $event)"
title="Edit"></div>
[title]="canWrite(item) ? 'Edit' : 'View Configuration'"></div>
}
@if (hasAdvancedUi(item)) {
<div

View File

@ -97,10 +97,6 @@ export class ParameterProvidersTable {
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
}
canConfigure(entity: ParameterProviderEntity): boolean {
return this.canRead(entity) && this.canWrite(entity);
}
hasAdvancedUi(entity: ParameterProviderEntity): boolean {
return this.canRead(entity) && !!entity.component.customUiUrl;
}

View File

@ -15,7 +15,14 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Edit Registry Client</h2>
<h2 mat-dialog-title>
<div class="flex justify-between items-baseline">
<div>Edit Registry Client</div>
<div class="text-sm">
{{ formatType(request.registryClient) }}
</div>
</div>
</h2>
<form class="edit-registry-client-form" [formGroup]="editRegistryClientForm">
<error-banner></error-banner>
<mat-dialog-content>
@ -35,9 +42,9 @@
</mat-form-field>
</div>
<div class="flex flex-col mb-5">
<div>Id</div>
<div>Type</div>
<div class="accent-color font-medium">
{{ request.registryClient.component.type }}
{{ formatType(request.registryClient) }}
</div>
</div>
<div>

View File

@ -17,6 +17,10 @@
@use '@angular/material' as mat;
h2.mdc-dialog__title::before {
height: 0;
}
.edit-registry-client-form {
@include mat.button-density(-1);

View File

@ -24,12 +24,11 @@ import { MatButtonModule } from '@angular/material/button';
import { AsyncPipe } from '@angular/common';
import { Observable } from 'rxjs';
import {
DocumentedType,
InlineServiceCreationRequest,
InlineServiceCreationResponse,
Parameter,
Property,
TextTipInput
RegistryClientEntity
} from '../../../../../state/shared';
import { EditRegistryClientDialogRequest, EditRegistryClientRequest } from '../../../state/registry-clients';
import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
@ -101,15 +100,8 @@ export class EditRegistryClient {
});
}
formatType(option: DocumentedType): string {
return this.nifiCommon.substringAfterLast(option.type, '.');
}
getOptionTipData(option: DocumentedType): TextTipInput {
return {
// @ts-ignore
text: option.description
};
formatType(entity: RegistryClientEntity): string {
return this.nifiCommon.formatType(entity.component);
}
submitForm(postUpdateNavigation?: string[]) {

View File

@ -31,7 +31,6 @@
<td mat-cell *matCellDef="let item">
@if (canRead(item)) {
<div class="flex items-center gap-x-2">
<!-- TODO - handle read only in configure component? -->
@if (hasErrors(item)) {
<div
class="mr-3 pointer fa fa-warning has-errors"

View File

@ -15,7 +15,16 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Edit Reporting Task</h2>
<h2 mat-dialog-title>
<div class="flex justify-between items-baseline">
<div>
{{ readonly ? 'Reporting Task Details' : 'Edit Reporting Task' }}
</div>
<div class="text-sm">
{{ formatType(request.reportingTask) }}
</div>
</div>
</h2>
<form class="reporting-task-edit-form" [formGroup]="editReportingTaskForm">
<error-banner></error-banner>
<mat-dialog-content>
@ -26,9 +35,11 @@
<div class="flex">
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput formControlName="name" type="text" />
<input matInput formControlName="name" type="text" [readonly]="readonly" />
</mat-form-field>
<mat-checkbox color="primary" formControlName="state" class="pl-1"> Enabled </mat-checkbox>
<mat-checkbox color="primary" formControlName="state" class="ml-1 mt-2">
Enabled
</mat-checkbox>
</div>
<div class="flex flex-col mb-5">
<div>Id</div>
@ -59,6 +70,7 @@
@for (option of strategies; track option) {
<mat-option
[value]="option.value"
[disabled]="readonly"
nifiTooltip
[tooltipComponentType]="TextTip"
[tooltipInputData]="getPropertyTipData(option)"
@ -76,7 +88,8 @@
matInput
formControlName="schedulingPeriod"
(change)="schedulingPeriodChanged()"
type="text" />
type="text"
[readonly]="readonly" />
</mat-form-field>
</div>
</div>
@ -100,7 +113,12 @@
<div class="tab-content py-4">
<mat-form-field>
<mat-label>Comments</mat-label>
<textarea matInput formControlName="comments" type="text" rows="5"></textarea>
<textarea
matInput
formControlName="comments"
type="text"
rows="5"
[readonly]="readonly"></textarea>
</mat-form-field>
</div>
</mat-tab>
@ -108,15 +126,19 @@
</mat-dialog-content>
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editReportingTaskForm.dirty || editReportingTaskForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
@if (readonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editReportingTaskForm.dirty || editReportingTaskForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
}
</mat-dialog-actions>
}
</form>

View File

@ -17,6 +17,10 @@
@use '@angular/material' as mat;
h2.mdc-dialog__title::before {
height: 0;
}
.reporting-task-edit-form {
@include mat.button-density(-1);

View File

@ -83,6 +83,8 @@ export class EditReportingTask {
new EventEmitter<UpdateReportingTaskRequest>();
editReportingTaskForm: FormGroup;
readonly: boolean;
schedulingStrategy: string;
cronDrivenSchedulingPeriod: string;
timerDrivenSchedulingPeriod: string;
@ -108,6 +110,11 @@ export class EditReportingTask {
private nifiCommon: NiFiCommon,
private clusterConnectionService: ClusterConnectionService
) {
this.readonly =
!request.reportingTask.permissions.canWrite ||
(request.reportingTask.status.runStatus !== 'STOPPED' &&
request.reportingTask.status.runStatus !== 'DISABLED');
const serviceProperties: any = request.reportingTask.component.properties;
const properties: Property[] = Object.entries(serviceProperties).map((entry: any) => {
const [property, value] = entry;
@ -137,13 +144,16 @@ export class EditReportingTask {
// 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),
state: new FormControl(
{ value: request.reportingTask.component.state !== 'DISABLED', disabled: this.readonly },
Validators.required
),
schedulingStrategy: new FormControl(
request.reportingTask.component.schedulingStrategy,
Validators.required
),
schedulingPeriod: new FormControl(schedulingPeriod, Validators.required),
properties: new FormControl(properties),
properties: new FormControl({ value: properties, disabled: this.readonly }),
comments: new FormControl(request.reportingTask.component.comments)
});
}

View File

@ -35,7 +35,6 @@
class="pointer fa fa-book primary-color"
(click)="viewDocumentationClicked(item, $event)"
title="View Documentation"></div>
<!-- TODO - handle read only in configure component? -->
@if (hasComments(item)) {
<div
class="pointer fa fa-comment primary-color"
@ -112,24 +111,22 @@
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center justify-end gap-x-2">
@if (canStop(item)) {
<div
class="pointer fa fa-stop primary-color"
(click)="stopClicked(item)"
title="Stop"></div>
}
@if (canEdit(item)) {
<div
class="pointer fa fa-cog primary-color"
(click)="configureClicked(item, $event)"
title="Edit"></div>
}
<div
class="pointer fa fa-cog primary-color"
(click)="configureClicked(item, $event)"
[title]="canEdit(item) ? 'Edit' : 'View Configuration'"></div>
@if (hasAdvancedUi(item)) {
<div
class="pointer fa fa-cogs primary-color"
(click)="advancedClicked(item, $event)"
title="Advanced"></div>
}
@if (canStop(item)) {
<div
class="pointer fa fa-stop primary-color"
(click)="stopClicked(item)"
title="Stop"></div>
}
@if (canStart(item)) {
<div
class="pointer fa fa-play primary-color"

View File

@ -81,7 +81,7 @@
<div class="flex items-center justify-end gap-x-2">
@if (canEditOrDelete(currentUser, item)) {
<div
class="pointer fa fa-pencil primary-color"
class="pointer fa fa-cog primary-color"
title="Edit"
(click)="editClicked(item, $event)"></div>
}

View File

@ -106,7 +106,8 @@
class="pointer fa fa-sticky-note-o primary-color"
nifiTooltip
[tooltipComponentType]="BulletinsTip"
[tooltipInputData]="getBulletinsTipData(reference)"></div>
[tooltipInputData]="getBulletinsTipData(reference)"
[delayClose]="false"></div>
}
@if (hasActiveThreads(reference.component)) {
<div>({{ reference.component.activeThreadCount }})</div>

View File

@ -118,19 +118,16 @@
<td mat-cell *matCellDef="let item">
@if (definedByCurrentGroup(item)) {
<div class="flex items-center justify-end gap-x-2">
@if (canConfigure(item)) {
<div
class="pointer fa fa-cog primary-color"
(click)="configureClicked(item, $event)"
title="Edit"></div>
}
<div
class="pointer fa fa-cog primary-color"
(click)="configureClicked(item, $event)"
[title]="canConfigure(item) ? 'Edit' : 'View Configuration'"></div>
@if (hasAdvancedUi(item)) {
<div
class="pointer fa fa-cogs primary-color"
(click)="advancedClicked(item, $event)"
title="Advanced"></div>
}
<!-- TODO - handle read only in configure component? -->
@if (canDisable(item)) {
<div
class="pointer icon icon-enable-false primary-color"

View File

@ -15,7 +15,16 @@
~ limitations under the License.
-->
<h2 mat-dialog-title>Edit Controller Service</h2>
<h2 mat-dialog-title>
<div class="flex justify-between items-baseline">
<div>
{{ readonly ? 'Controller Service Details' : 'Edit Controller Service' }}
</div>
<div class="text-sm">
{{ formatType(request.controllerService) }}
</div>
</div>
</h2>
<form class="controller-service-edit-form" [formGroup]="editControllerServiceForm">
<error-banner></error-banner>
<mat-dialog-content>
@ -26,7 +35,7 @@
<div>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput formControlName="name" type="text" />
<input matInput formControlName="name" type="text" [readonly]="readonly" />
</mat-form-field>
</div>
<div class="flex flex-col mb-5">
@ -69,7 +78,7 @@
<mat-label>Bulletin Level</mat-label>
<mat-select formControlName="bulletinLevel">
@for (option of bulletinLevels; track option) {
<mat-option [value]="option.value">
<mat-option [value]="option.value" [disabled]="readonly">
{{ option.text }}
</mat-option>
}
@ -112,7 +121,12 @@
<div class="tab-content py-4">
<mat-form-field>
<mat-label>Comments</mat-label>
<textarea matInput formControlName="comments" type="text" rows="5"></textarea>
<textarea
matInput
formControlName="comments"
type="text"
rows="5"
[readonly]="readonly"></textarea>
</mat-form-field>
</div>
</mat-tab>
@ -120,15 +134,19 @@
</mat-dialog-content>
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editControllerServiceForm.dirty || editControllerServiceForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
@if (readonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editControllerServiceForm.dirty || editControllerServiceForm.invalid || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
}
</mat-dialog-actions>
}
</form>

View File

@ -17,6 +17,10 @@
@use '@angular/material' as mat;
h2.mdc-dialog__title::before {
height: 0;
}
.controller-service-edit-form {
@include mat.button-density(-1);

View File

@ -82,6 +82,7 @@ export class EditControllerService {
new EventEmitter<UpdateControllerServiceRequest>();
editControllerServiceForm: FormGroup;
readonly: boolean;
bulletinLevels = [
{
@ -113,6 +114,10 @@ export class EditControllerService {
private nifiCommon: NiFiCommon,
private clusterConnectionService: ClusterConnectionService
) {
this.readonly =
!request.controllerService.permissions.canWrite ||
request.controllerService.status.runStatus !== 'DISABLED';
const serviceProperties: any = request.controllerService.component.properties;
const properties: Property[] = Object.entries(serviceProperties).map((entry: any) => {
const [property, value] = entry;
@ -127,7 +132,7 @@ export class EditControllerService {
this.editControllerServiceForm = this.formBuilder.group({
name: new FormControl(request.controllerService.component.name, Validators.required),
bulletinLevel: new FormControl(request.controllerService.component.bulletinLevel, Validators.required),
properties: new FormControl(properties),
properties: new FormControl({ value: properties, disabled: this.readonly }),
comments: new FormControl(request.controllerService.component.comments)
});
}

View File

@ -31,6 +31,7 @@
<ng-container *ngIf="allowableValue.description; else noDescription">
<mat-option
[value]="allowableValue.id"
[disabled]="readonly"
(mousedown)="preventDrag($event)"
nifiTooltip
[tooltipComponentType]="TextTip"
@ -45,7 +46,10 @@
</mat-option>
</ng-container>
<ng-template #noDescription>
<mat-option [value]="allowableValue.id" (mousedown)="preventDrag($event)">
<mat-option
[value]="allowableValue.id"
[disabled]="readonly"
(mousedown)="preventDrag($event)">
<span
class="option-text"
[class.nifi-surface-default]="allowableValue.value == null"
@ -73,6 +77,7 @@
<ng-container *ngIf="parameterAllowableValue.description; else noDescription">
<mat-option
[value]="parameterAllowableValue.id"
[disabled]="readonly"
(mousedown)="preventDrag($event)"
nifiTooltip
[tooltipComponentType]="TextTip"
@ -87,7 +92,10 @@
</mat-option>
</ng-container>
<ng-template #noDescription>
<mat-option [value]="parameterAllowableValue.id" (mousedown)="preventDrag($event)">
<mat-option
[value]="parameterAllowableValue.id"
[disabled]="readonly"
(mousedown)="preventDrag($event)">
<span
class="option-text"
[class.unset]="parameterAllowableValue.value == null"
@ -102,18 +110,29 @@
</ng-template>
</div>
<div class="flex justify-end items-center gap-x-2">
<button mat-button type="button" (mousedown)="preventDrag($event)" (click)="cancelClicked()">
Cancel
</button>
<button
[disabled]="!comboEditorForm.dirty || comboEditorForm.invalid"
(mousedown)="preventDrag($event)"
type="button"
color="primary"
(click)="okClicked()"
mat-button>
Ok
</button>
@if (readonly) {
<button
mat-button
type="button"
color="primary"
(mousedown)="preventDrag($event)"
(click)="cancelClicked()">
Close
</button>
} @else {
<button mat-button type="button" (mousedown)="preventDrag($event)" (click)="cancelClicked()">
Cancel
</button>
<button
[disabled]="!comboEditorForm.dirty || comboEditorForm.invalid"
(mousedown)="preventDrag($event)"
type="button"
color="primary"
(click)="okClicked()"
mat-button>
Ok
</button>
}
</div>
</div>
</form>

View File

@ -84,6 +84,7 @@ export class ComboEditor {
this.initialAllowableValues();
}
@Input() width!: number;
@Input() readonly: boolean = false;
@Output() ok: EventEmitter<any> = new EventEmitter<any>();
@Output() cancel: EventEmitter<void> = new EventEmitter<void>();

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<div class="property-editor p-4" [style.width.px]="width" cdkDrag resizable (resized)="resized()">
<div class="property-editor p-4 h-full" [style.width.px]="width" cdkDrag resizable (resized)="resized()">
<form class="h-full" [formGroup]="nfEditorForm" cdkTrapFocus cdkTrapFocusAutoCapture>
<div class="flex flex-col gap-y-3 h-full">
<div class="flex justify-end">
@ -50,27 +50,40 @@
(mousedown)="preventDrag($event)"
(codeMirrorLoaded)="codeMirrorLoaded($event)"></ngx-codemirror>
</div>
<mat-checkbox
color="primary"
formControlName="setEmptyString"
(mousedown)="preventDrag($event)"
(change)="setEmptyStringChanged()"
>Set empty string</mat-checkbox
>
@if (!readonly) {
<mat-checkbox
color="primary"
formControlName="setEmptyString"
(mousedown)="preventDrag($event)"
(change)="setEmptyStringChanged()"
>Set empty string
</mat-checkbox>
}
</div>
<div class="flex justify-end items-center gap-x-2">
<button mat-button type="button" (mousedown)="preventDrag($event)" (click)="cancelClicked()">
Cancel
</button>
<button
[disabled]="!nfEditorForm.dirty || nfEditorForm.invalid"
(mousedown)="preventDrag($event)"
type="button"
color="primary"
(click)="okClicked()"
mat-button>
Ok
</button>
@if (readonly) {
<button
mat-button
type="button"
color="primary"
(mousedown)="preventDrag($event)"
(click)="cancelClicked()">
Close
</button>
} @else {
<button mat-button type="button" (mousedown)="preventDrag($event)" (click)="cancelClicked()">
Cancel
</button>
<button
[disabled]="!nfEditorForm.dirty || nfEditorForm.invalid"
(mousedown)="preventDrag($event)"
type="button"
color="primary"
(click)="okClicked()"
mat-button>
Ok
</button>
}
</div>
</div>
</form>

View File

@ -82,6 +82,7 @@ export class NfEditor implements OnDestroy {
this.loadParameters();
}
@Input() width!: number;
@Input() readonly: boolean = false;
@Output() ok: EventEmitter<string> = new EventEmitter<string>();
@Output() cancel: EventEmitter<void> = new EventEmitter<void>();
@ -116,7 +117,7 @@ export class NfEditor implements OnDestroy {
codeMirrorLoaded(codeEditor: any): void {
this.editor = codeEditor.codeMirror;
this.editor.setSize('100%', 100);
this.editor.setSize('100%', '100%');
this.editor.execCommand('selectAll');
}
@ -161,6 +162,7 @@ export class NfEditor implements OnDestroy {
getOptions(): any {
return {
mode: this.mode,
readOnly: this.readonly,
lineNumbers: true,
matchBrackets: true,
extraKeys: {

View File

@ -18,12 +18,14 @@
<div class="property-table flex flex-col gap-y-3">
<div class="flex justify-between items-center">
<div class="font-bold">Required field</div>
<div>
<!-- TODO Property Verification -->
<button mat-icon-button color="primary" type="button" (click)="newPropertyClicked()">
<i class="fa fa-plus"></i>
</button>
</div>
@if (!isDisabled) {
<div>
<!-- TODO Property Verification -->
<button mat-icon-button color="primary" type="button" (click)="newPropertyClicked()">
<i class="fa fa-plus"></i>
</button>
</div>
}
</div>
<div class="listing-table">
<div class="h-96 overflow-y-auto overflow-x-hidden">
@ -101,18 +103,6 @@
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center justify-end gap-x-2">
@if (item.descriptor.identifiesControllerService) {
<div
class="pointer fa fa-plus primary-color"
(click)="createNewControllerService(item)"
title="Create new service"></div>
}
@if (canConvertToParameter(item)) {
<div
class="pointer fa fa-level-up primary-color"
(click)="convertToParameterClicked(item)"
title="Convert to Parameter"></div>
}
@if (canGoToParameter(item)) {
<div
class="pointer fa fa-long-arrow-right primary-color"
@ -125,11 +115,25 @@
(click)="goToServiceClicked(item)"
title="Go to Service"></div>
}
@if (item.type == 'userDefined') {
<div
class="pointer fa fa-trash primary-color"
(click)="deleteProperty(item)"
title="Delete"></div>
@if (!isDisabled) {
@if (item.descriptor.identifiesControllerService) {
<div
class="pointer fa fa-plus primary-color"
(click)="createNewControllerService(item)"
title="Create new service"></div>
}
@if (canConvertToParameter(item)) {
<div
class="pointer fa fa-level-up primary-color"
(click)="convertToParameterClicked(item)"
title="Convert to Parameter"></div>
}
@if (item.type == 'userDefined') {
<div
class="pointer fa fa-trash primary-color"
(click)="deleteProperty(item)"
title="Delete"></div>
}
}
</div>
</td>
@ -157,6 +161,7 @@
[item]="editorItem"
[getParameters]="getParameters"
[width]="editorWidth"
[readonly]="isDisabled"
(ok)="savePropertyValue(editorItem, $event)"
(cancel)="closeEditor()"></combo-editor>
} @else {
@ -164,6 +169,7 @@
[item]="editorItem"
[getParameters]="getParameters"
[width]="editorWidth"
[readonly]="isDisabled"
(ok)="savePropertyValue(editorItem, $event)"
(cancel)="closeEditor()"></nf-editor>
}

View File

@ -244,7 +244,6 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
}
setDisabledState(isDisabled: boolean): void {
// TODO - update component to disable controls accordingly
this.isDisabled = isDisabled;
}
@ -410,8 +409,8 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
this.editorOpen = true;
if (this.hasAllowableValues(item)) {
this.editorWidth = width;
this.editorOffsetX = -24;
this.editorWidth = width + 50;
this.editorOffsetX = 0;
this.editorOffsetY = 24;
} else {
this.editorWidth = width + 100;
@ -431,8 +430,6 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
}
canGoToService(item: PropertyItem): boolean {
// TODO - add Input() for supportsGoTo? currently only false in summary table
const descriptor: PropertyDescriptor = item.descriptor;
if (item.value && descriptor.identifiesControllerService && descriptor.allowableValues) {
return descriptor.allowableValues.some(

View File

@ -100,6 +100,10 @@
font-weight: normal !important;
}
.mat-mdc-tab-header {
user-select: none;
}
.mat-mdc-icon-button {
--mdc-icon-button-state-layer-size: 28px;
--mdc-icon-button-icon-size: 14px;