NIFI-13081: Adding support to edit a label (#8681)

* NIFI-13081:
- Adding support to edit a label.

* NIFI-13081:
- Fixing lint issues.

This closes #8681
This commit is contained in:
Matt Gilman 2024-04-22 16:05:28 -04:00 committed by GitHub
parent 72a3529718
commit 830b7443d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 334 additions and 1 deletions

View File

@ -1213,7 +1213,7 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
},
{
condition: (selection: d3.Selection<any, any, any, any>) => {
return this.canvasUtils.isDisconnected(selection);
return this.canvasUtils.isDisconnected(selection) && this.canvasUtils.canModify(selection);
},
clazz: 'fa icon-group',
text: 'Group',

View File

@ -426,6 +426,11 @@ export const openEditRemoteProcessGroupDialog = createAction(
props<{ request: EditComponentDialogRequest }>()
);
export const openEditLabelDialog = createAction(
`${CANVAS_PREFIX} Open Edit Label Dialog`,
props<{ request: EditComponentDialogRequest }>()
);
export const navigateToManageRemotePorts = createAction(
`${CANVAS_PREFIX} Open Remote Process Group Manage Remote Ports`,
props<{ request: RpgManageRemotePortsRequest }>()

View File

@ -129,6 +129,7 @@ import { ExtensionTypesService } from '../../../../service/extension-types.servi
import { ChangeComponentVersionDialog } from '../../../../ui/common/change-component-version-dialog/change-component-version-dialog';
import { SnippetService } from '../../service/snippet.service';
import { selectTransform } from '../transform/transform.selectors';
import { EditLabel } from '../../ui/canvas/items/label/edit-label/edit-label.component';
@Injectable()
export class FlowEffects {
@ -1053,6 +1054,8 @@ export class FlowEffects {
case ComponentType.InputPort:
case ComponentType.OutputPort:
return of(FlowActions.openEditPortDialog({ request }));
case ComponentType.Label:
return of(FlowActions.openEditLabelDialog({ request }));
default:
return of(FlowActions.flowApiError({ error: 'Unsupported type of Component.' }));
}
@ -1119,6 +1122,38 @@ export class FlowEffects {
{ dispatch: false }
);
openEditLabelDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(FlowActions.openEditLabelDialog),
map((action) => action.request),
tap((request) => {
this.dialog
.open(EditLabel, {
...MEDIUM_DIALOG,
data: request
})
.afterClosed()
.subscribe(() => {
this.store.dispatch(FlowActions.clearFlowApiError());
this.store.dispatch(
FlowActions.selectComponents({
request: {
components: [
{
id: request.entity.id,
componentType: request.type
}
]
}
})
);
});
})
),
{ dispatch: false }
);
openEditProcessorDialog$ = createEffect(
() =>
this.actions$.pipe(

View File

@ -0,0 +1,58 @@
<!--
~ 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>{{ readonly ? 'Label Details' : 'Edit Label' }}</h2>
<form class="edit-label-form" [formGroup]="editLabelForm">
<error-banner></error-banner>
<mat-dialog-content>
<div>
<mat-form-field>
<mat-label>Label Value</mat-label>
<textarea matInput formControlName="value" type="text" rows="5" [readonly]="readonly"></textarea>
</mat-form-field>
</div>
<div>
<mat-form-field subscriptSizing="dynamic">
<mat-label>Font Size</mat-label>
<mat-select formControlName="fontSize">
@for (option of fontSizeOptions; track option) {
<mat-option [value]="option.value" [disabled]="readonly">
{{ option.text }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</mat-dialog-content>
@if ({ value: (saving$ | async)! }; as saving) {
<mat-dialog-actions align="end">
@if (readonly) {
<button mat-button mat-dialog-close color="primary">Close</button>
} @else {
<button mat-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editLabelForm.dirty || editLabelForm.invalid || saving.value"
type="button"
color="primary"
(click)="editLabel()"
mat-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
}
</mat-dialog-actions>
}
</form>

View File

@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@use '@angular/material' as mat;
.edit-label-form {
@include mat.button-density(-1);
.mat-mdc-form-field {
width: 100%;
}
}

View File

@ -0,0 +1,93 @@
/*
* 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 { EditLabel } from './edit-label.component';
import { EditComponentDialogRequest } from '../../../../../state/flow';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ComponentType } from '../../../../../../../state/shared';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../../../state/flow/flow.reducer';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ClusterConnectionService } from '../../../../../../../service/cluster-connection.service';
describe('EditLabel', () => {
let component: EditLabel;
let fixture: ComponentFixture<EditLabel>;
const data: EditComponentDialogRequest = {
type: ComponentType.Label,
uri: 'https://localhost:4200/nifi-api/labels/d7c98831-018e-1000-6a26-6d74b57897e5',
entity: {
revision: {
version: 0
},
id: 'd7c98831-018e-1000-6a26-6d74b57897e5',
uri: 'https://localhost:4200/nifi-api/labels/d7c98831-018e-1000-6a26-6d74b57897e5',
position: {
x: 424,
y: -432
},
permissions: {
canRead: true,
canWrite: true
},
dimensions: {
width: 150,
height: 150
},
zIndex: 4,
component: {
id: 'd7c98831-018e-1000-6a26-6d74b57897e5',
versionedComponentId: 'c99e7867-73c6-3687-8f27-9b1ee5d761c4',
parentGroupId: 'f588fccd-018d-1000-e6dd-56d56dead186',
position: {
x: 424,
y: -432
},
width: 150,
height: 150,
zIndex: 4,
style: {}
}
}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [EditLabel, NoopAnimationsModule],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: data },
provideMockStore({ initialState }),
{
provide: ClusterConnectionService,
useValue: {
isDisconnectionAcknowledged: jest.fn()
}
}
]
});
fixture = TestBed.createComponent(EditLabel);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,116 @@
/*
* 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, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { CanvasState } from '../../../../../state';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { updateComponent } from '../../../../../state/flow/flow.actions';
import { Client } from '../../../../../../../service/client.service';
import { EditComponentDialogRequest } from '../../../../../state/flow';
import { SelectOption } from '../../../../../../../state/shared';
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
import { MatInputModule } from '@angular/material/input';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
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 { MatOption } from '@angular/material/autocomplete';
import { MatSelect } from '@angular/material/select';
import { NifiTooltipDirective } from '../../../../../../../ui/common/tooltips/nifi-tooltip.directive';
@Component({
selector: 'edit-label',
standalone: true,
templateUrl: './edit-label.component.html',
imports: [
ReactiveFormsModule,
ErrorBanner,
MatDialogModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
AsyncPipe,
NifiSpinnerDirective,
MatOption,
MatSelect,
NifiTooltipDirective
],
styleUrls: ['./edit-label.component.scss']
})
export class EditLabel {
saving$ = this.store.select(selectSaving);
editLabelForm: FormGroup;
readonly: boolean;
fontSizeOptions: SelectOption[] = [12, 14, 16, 18, 20, 22, 24].map((fontSize) => {
return {
text: `${fontSize}px`,
value: `${fontSize}px`
};
});
constructor(
@Inject(MAT_DIALOG_DATA) public request: EditComponentDialogRequest,
private formBuilder: FormBuilder,
private store: Store<CanvasState>,
private client: Client,
private clusterConnectionService: ClusterConnectionService
) {
this.readonly = !request.entity.permissions.canWrite;
let fontSize = this.fontSizeOptions[0].value;
if (request.entity.component.style['font-size']) {
fontSize = request.entity.component.style['font-size'];
}
// build the form
this.editLabelForm = this.formBuilder.group({
value: new FormControl(request.entity.component.label, Validators.required),
fontSize: new FormControl(fontSize, Validators.required)
});
}
editLabel() {
const payload: any = {
revision: this.client.getRevision(this.request.entity),
disconnectedNodeAcknowledged: this.clusterConnectionService.isDisconnectionAcknowledged(),
component: {
id: this.request.entity.id,
label: this.editLabelForm.get('value')?.value,
style: {
'font-size': this.editLabelForm.get('fontSize')?.value
}
}
};
this.store.dispatch(
updateComponent({
request: {
id: this.request.entity.id,
type: this.request.type,
uri: this.request.uri,
payload
}
})
);
}
}