[NIFI-12665] fetch parameter provider parameters (#8367)

* [NIFI-12665] - fetch parameter provider parameters
* add permissions checks to parameter provider table actions for edit, delete, and fetch.
* error handling
* routing to fetch parameter provider dialog
* list the parameter groups
* parameter sensitivity selction/deselection
* show banner error if any affected/referencing components are not readable and writeable
* add indicators to parameters for changed, new, removed, ...
* refactored parameter-references component to the common area. leveraged it in the fetch dialog.
* validate the fetch form
* submit the fetch parameter provider parameters

* make the async update step completion icon color theme-aware

* add missing license header

* fixes for the initial round of review comments

* fixing issues found in review

* fix registry clients test

* stop polling when there is an api error

* use sort and join pipes in a couple of more places.

* protect references to parameter provider in the context for read permissions

* when full screen error is triggered, close any open dialog with the 'ROUTED' result to prevent unintended afterClosed actions taking place (like re-selection)

* handle fetch parameter provider error

* remove TODO comments

* call fullScreenError correctly

This closes #8367
This commit is contained in:
Rob Fellows 2024-02-09 13:19:12 -05:00 committed by GitHub
parent 16eadc88da
commit 7dc696ecdc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 2470 additions and 175 deletions

View File

@ -28,6 +28,7 @@
$accent-palette: map.get($color-config, 'accent');
$warn-palette: map.get($color-config, 'warn');
$canvas-primary-palette: map.get($canvas-color-config, 'primary');
$canvas-accent-palette: map.get($canvas-color-config, 'accent');
// Get hues from palette
$primary-palette-300: mat.get-color-from-palette($primary-palette, 300);
@ -35,6 +36,7 @@
$accent-palette-A400: mat.get-color-from-palette($accent-palette, 'A400');
$warn-palette-500: mat.get-color-from-palette($warn-palette, 500);
$canvas-primary-palette-A200: mat.get-color-from-palette($canvas-primary-palette, 'A200');
$canvas-accent-palette-500: mat.get-color-from-palette($canvas-accent-palette, 500);
.splash {
background-color: $primary-palette-500;
@ -54,4 +56,8 @@
.nifi-snackbar .mat-mdc-button:not(:disabled) .mdc-button__label {
color: $accent-palette-A400;
}
.fa.fa-check.complete {
color: $canvas-accent-palette-500;
}
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, Inject } from '@angular/core';
import { Component } from '@angular/core';
import { GuardsCheckEnd, GuardsCheckStart, NavigationCancel, Router } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Storage } from './service/storage.service';

View File

@ -44,6 +44,7 @@ import { FlowConfigurationEffects } from './state/flow-configuration/flow-config
import { ComponentStateEffects } from './state/component-state/component-state.effects';
import { ErrorEffects } from './state/error/error.effects';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { PipesModule } from './pipes/pipes.module';
@NgModule({
declarations: [AppComponent],
@ -80,7 +81,8 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
MatProgressSpinnerModule,
MatNativeDateModule,
MatDialogModule,
MatSnackBarModule
MatSnackBarModule,
PipesModule
],
providers: [
{

View File

@ -15,7 +15,17 @@
* limitations under the License.
*/
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import {
AfterViewInit,
Component,
DestroyRef,
ElementRef,
EventEmitter,
inject,
Input,
Output,
ViewChild
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@ -27,6 +37,7 @@ import { BulletinBoardEvent, BulletinBoardFilterArgs, BulletinBoardItem } from '
import { BulletinEntity, ComponentType } from '../../../../../state/shared';
import { debounceTime, delay, Subject } from 'rxjs';
import { RouterLink } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'bulletin-board-list',
@ -51,6 +62,7 @@ export class BulletinBoardList implements AfterViewInit {
private bulletinsChanged$: Subject<void> = new Subject<void>();
private _items: BulletinBoardItem[] = [];
private destroyRef: DestroyRef = inject(DestroyRef);
@ViewChild('scrollContainer') private scroll!: ElementRef;
@ -58,6 +70,7 @@ export class BulletinBoardList implements AfterViewInit {
this._items = items;
this.bulletinsChanged$.next();
}
get bulletinBoardItems(): BulletinBoardItem[] {
return this._items;
}
@ -74,16 +87,19 @@ export class BulletinBoardList implements AfterViewInit {
ngAfterViewInit(): void {
this.filterForm
.get('filterTerm')
?.valueChanges.pipe(debounceTime(500))
?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
.subscribe((filterTerm: string) => {
const filterColumn = this.filterForm.get('filterColumn')?.value;
this.applyFilter(filterTerm, filterColumn);
});
this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
this.applyFilter(filterTerm, filterColumn);
});
this.filterForm
.get('filterColumn')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
this.applyFilter(filterTerm, filterColumn);
});
// scroll the initial chuck of bulletins
this.scrollToBottom();

View File

@ -15,13 +15,14 @@
* limitations under the License.
*/
import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
import { AfterViewInit, Component, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core';
import { CounterEntity } from '../../../state/counter-listing';
import { MatTableDataSource } from '@angular/material/table';
import { Sort } from '@angular/material/sort';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounceTime } from 'rxjs';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'counter-table',
@ -30,6 +31,7 @@ import { NiFiCommon } from '../../../../../service/nifi-common.service';
})
export class CounterTable implements AfterViewInit {
private _canModifyCounters = false;
private destroyRef: DestroyRef = inject(DestroyRef);
filterTerm = '';
filterColumn: 'context' | 'name' = 'name';
totalCount = 0;
@ -95,16 +97,19 @@ export class CounterTable implements AfterViewInit {
ngAfterViewInit(): void {
this.filterForm
.get('filterTerm')
?.valueChanges.pipe(debounceTime(500))
?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
.subscribe((filterTerm: string) => {
const filterColumn = this.filterForm.get('filterColumn')?.value;
this.applyFilter(filterTerm, filterColumn);
});
this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
this.applyFilter(filterTerm, filterColumn);
});
this.filterForm
.get('filterColumn')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
this.applyFilter(filterTerm, filterColumn);
});
}
applyFilter(filterTerm: string, filterColumn: string) {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { Component, DestroyRef, ElementRef, inject, Input, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { initialState } from '../../../../state/flow/flow.reducer';
import { debounceTime, filter, switchMap, tap } from 'rxjs';
@ -70,6 +70,7 @@ export class Search implements OnInit {
overlayY: 'top'
};
private position: ConnectionPositionPair = new ConnectionPositionPair(this.originPos, this.overlayPos, 0, 2);
private destroyRef: DestroyRef = inject(DestroyRef);
public positions: ConnectionPositionPair[] = [this.position];
searchForm: FormGroup;
@ -116,6 +117,7 @@ export class Search implements OnInit {
this.searchForm
.get('searchBar')
?.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef),
filter((data) => data?.trim().length > 0),
debounceTime(500),
tap(() => (this.searching = true)),

View File

@ -19,6 +19,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoRegistryClientsDialog } from './no-registry-clients-dialog.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
describe('NoRegistryClientsDialog', () => {
let component: NoRegistryClientsDialog;
@ -26,7 +28,7 @@ describe('NoRegistryClientsDialog', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NoRegistryClientsDialog],
imports: [NoRegistryClientsDialog, RouterModule, RouterTestingModule],
providers: [
{
provide: MAT_DIALOG_DATA,

View File

@ -19,6 +19,7 @@ import {
ParameterContextReferenceEntity,
ParameterContextUpdateRequestEntity,
ParameterEntity,
ParameterProviderConfigurationEntity,
Permissions,
Revision
} from '../../../../state/shared';
@ -73,7 +74,7 @@ export interface ParameterContext {
parameters: ParameterEntity[];
boundProcessGroups: BoundProcessGroup[];
inheritedParameterContexts: ParameterContextReferenceEntity[];
// private ParameterProviderConfigurationEntity parameterProviderConfiguration;
parameterProviderConfiguration?: ParameterProviderConfigurationEntity;
}
// TODO - Replace this with ProcessGroupEntity was available

View File

@ -26,7 +26,7 @@
*ngFor="let updateStep of requestEntity.request.updateSteps"
class="flex justify-between items-center">
<div class="value">{{ updateStep.description }}</div>
<div *ngIf="updateStep.complete; else stepInProgress" class="fa fa-check text-green-500"></div>
<div *ngIf="updateStep.complete; else stepInProgress" class="fa fa-check complete"></div>
<ng-template #stepInProgress>
<div class="fa fa-spin fa-circle-o-notch"></div>
</ng-template>
@ -67,6 +67,14 @@
<div>Id</div>
<div class="value">{{ request.parameterContext?.id }}</div>
</div>
<div class="flex flex-col mb-5" *ngIf="parameterProvider">
<div>Parameter Provider</div>
<a [routerLink]="getParameterProviderLink(parameterProvider)" mat-dialog-close="ROUTED">
{{ parameterProvider.parameterGroupName }}
from
{{ parameterProvider.parameterProviderName }}
</a>
</div>
<div>
<mat-form-field>
<mat-label>Name</mat-label>
@ -97,6 +105,7 @@
<div class="tab-content py-4">
<parameter-table
formControlName="parameters"
[canAddParameters]="!request.parameterContext?.component?.parameterProviderConfiguration"
[createNewParameter]="createNewParameter"
[editParameter]="editParameter"></parameter-table>
</div>

View File

@ -30,10 +30,16 @@ import { EditParameterContextRequest, ParameterContextEntity } from '../../../st
import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
import { Client } from '../../../../../service/client.service';
import { ParameterTable } from '../parameter-table/parameter-table.component';
import { Parameter, ParameterContextUpdateRequestEntity, ParameterEntity } from '../../../../../state/shared';
import {
Parameter,
ParameterContextUpdateRequestEntity,
ParameterEntity,
ParameterProviderConfiguration
} from '../../../../../state/shared';
import { ProcessGroupReferences } from '../process-group-references/process-group-references.component';
import { ParameterContextInheritance } from '../parameter-context-inheritance/parameter-context-inheritance.component';
import { ParameterReferences } from '../parameter-references/parameter-references.component';
import { ParameterReferences } from '../../../../../ui/common/parameter-references/parameter-references.component';
import { RouterLink } from '@angular/router';
@Component({
selector: 'edit-parameter-context',
@ -56,7 +62,8 @@ import { ParameterReferences } from '../parameter-references/parameter-reference
ParameterTable,
ProcessGroupReferences,
ParameterContextInheritance,
ParameterReferences
ParameterReferences,
RouterLink
],
styleUrls: ['./edit-parameter-context.component.scss']
})
@ -72,6 +79,7 @@ export class EditParameterContext {
editParameterContextForm: FormGroup;
isNew: boolean;
parameterProvider: ParameterProviderConfiguration | null = null;
parameters!: ParameterEntity[];
@ -91,6 +99,9 @@ export class EditParameterContext {
request.parameterContext.component.inheritedParameterContexts
)
});
if (request.parameterContext.component.parameterProviderConfiguration) {
this.parameterProvider = request.parameterContext.component.parameterProviderConfiguration.component;
}
} else {
this.isNew = true;
@ -153,4 +164,8 @@ export class EditParameterContext {
this.editParameterContext.next(payload);
}
}
getParameterProviderLink(parameterProvider: ParameterProviderConfiguration): string[] {
return ['/settings', 'parameter-providers', parameterProvider.parameterProviderId];
}
}

View File

@ -27,7 +27,7 @@ import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { ParameterContextReferenceEntity, TextTipInput } from '../../../../../state/shared';
import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive';
import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
import { ParameterReferences } from '../parameter-references/parameter-references.component';
import { ParameterReferences } from '../../../../../ui/common/parameter-references/parameter-references.component';
import { ParameterContextEntity } from '../../../state/parameter-context-listing';
import {
DragDropModule,

View File

@ -83,7 +83,12 @@
(click)="$event.stopPropagation()"
[routerLink]="getPolicyLink(item)"
title="Access Policies"></div>
<!-- TODO go to parameter provider -->
<div
class="pointer fa fa-long-arrow-right"
*ngIf="canGoToParameterProvider(item)"
(click)="$event.stopPropagation()"
[routerLink]="getParameterProviderLink(item)"
title="Go to Parameter Provider"></div>
</div>
</td>
</ng-container>

View File

@ -22,6 +22,7 @@ import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { ParameterContextEntity } from '../../../state/parameter-context-listing';
import { FlowConfiguration } from '../../../../../state/flow-configuration';
import { CurrentUser } from '../../../../../state/current-user';
import { ParameterProviderConfigurationEntity } from '../../../../../state/shared';
@Component({
selector: 'parameter-context-table',
@ -66,7 +67,14 @@ export class ParameterContextTable {
}
formatProvider(entity: ParameterContextEntity): string {
return '';
if (!this.canRead(entity)) {
return '';
}
const paramProvider = entity.component.parameterProviderConfiguration;
if (!paramProvider) {
return '';
}
return `${paramProvider.component.parameterGroupName} from ${paramProvider.component.parameterProviderName}`;
}
formatDescription(entity: ParameterContextEntity): string {
@ -94,6 +102,20 @@ export class ParameterContextTable {
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
}
canGoToParameterProvider(entity: ParameterContextEntity): boolean {
if (!this.canRead(entity)) {
return false;
}
return !!entity.component.parameterProviderConfiguration;
}
getParameterProviderLink(entity: ParameterContextEntity): string[] {
if (!entity.component.parameterProviderConfiguration) {
return [];
}
return ['/settings', 'parameter-providers', entity.component.parameterProviderConfiguration.id];
}
getPolicyLink(entity: ParameterContextEntity): string[] {
return ['/access-policies', 'read', 'component', 'parameter-contexts', entity.id];
}

View File

@ -17,7 +17,7 @@
<div class="parameter-table listing-table flex gap-x-3">
<div class="flex flex-col gap-y-3" style="flex-grow: 3">
<div class="flex justify-end items-center">
<div class="flex justify-end items-center" *ngIf="canAddParameters">
<button class="nifi-button" type="button" (click)="newParameterClicked()">
<i class="fa fa-plus"></i>
</button>

View File

@ -28,7 +28,7 @@ import { Parameter, ParameterEntity, TextTipInput } from '../../../../../state/s
import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive';
import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
import { Observable, take } from 'rxjs';
import { ParameterReferences } from '../parameter-references/parameter-references.component';
import { ParameterReferences } from '../../../../../ui/common/parameter-references/parameter-references.component';
import { Store } from '@ngrx/store';
import { ParameterContextListingState } from '../../../state/parameter-context-listing';
import { showOkDialog } from '../../../../flow-designer/state/flow/flow.actions';
@ -69,6 +69,7 @@ export interface ParameterItem {
export class ParameterTable implements AfterViewInit, ControlValueAccessor {
@Input() createNewParameter!: (existingParameters: string[]) => Observable<Parameter>;
@Input() editParameter!: (parameter: Parameter) => Observable<Parameter>;
@Input() canAddParameters = true;
protected readonly TextTip = TextTip;

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { AfterViewInit, Component, DestroyRef, EventEmitter, inject, Input, Output, ViewChild } from '@angular/core';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
@ -37,6 +37,7 @@ import { Lineage, LineageRequest } from '../../../state/lineage';
import { LineageComponent } from './lineage/lineage.component';
import { GoToProvenanceEventSourceRequest, ProvenanceEventRequest } from '../../../state/provenance-event-listing';
import { MatSliderModule } from '@angular/material/slider';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'provenance-event-table',
@ -152,6 +153,7 @@ export class ProvenanceEventTable implements AfterViewInit {
protected readonly TextTip = TextTip;
protected readonly BulletinsTip = BulletinsTip;
protected readonly ValidationErrorsTip = ValidationErrorsTip;
private destroyRef: DestroyRef = inject(DestroyRef);
// TODO - conditionally include the cluster column
displayedColumns: string[] = [
@ -202,17 +204,20 @@ export class ProvenanceEventTable implements AfterViewInit {
this.filterForm
.get('filterTerm')
?.valueChanges.pipe(debounceTime(500))
?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
.subscribe((filterTerm: string) => {
const filterColumn = this.filterForm.get('filterColumn')?.value;
this.filterApplied = filterTerm.length > 0;
this.applyFilter(filterTerm, filterColumn);
});
this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
this.applyFilter(filterTerm, filterColumn);
});
this.filterForm
.get('filterColumn')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
this.applyFilter(filterTerm, filterColumn);
});
}
updateSort(sort: Sort): void {

View File

@ -24,7 +24,10 @@ import {
ConfigureParameterProviderRequest,
CreateParameterProviderRequest,
DeleteParameterProviderRequest,
ParameterProviderEntity
FetchParameterProviderParametersRequest,
ParameterProviderApplyParametersRequest,
ParameterProviderEntity,
ParameterProviderParameterApplicationEntity
} from '../state/parameter-providers';
import { PropertyDescriptorRetriever } from '../../../state/shared';
@ -75,4 +78,34 @@ export class ParameterProviderService implements PropertyDescriptorRetriever {
updateParameterProvider(configureRequest: ConfigureParameterProviderRequest): Observable<any> {
return this.httpClient.put(this.nifiCommon.stripProtocol(configureRequest.uri), configureRequest.payload);
}
fetchParameters(request: FetchParameterProviderParametersRequest): Observable<any> {
return this.httpClient.post(
`${ParameterProviderService.API}/parameter-providers/${request.id}/parameters/fetch-requests`,
{
id: request.id,
revision: request.revision
},
{ params: { disconnectedNodeAcknowledged: false } }
);
}
applyParameters(request: ParameterProviderParameterApplicationEntity): Observable<any> {
return this.httpClient.post(
`${ParameterProviderService.API}/parameter-providers/${request.id}/apply-parameters-requests`,
request
);
}
pollParameterProviderParametersUpdateRequest(
updateRequest: ParameterProviderApplyParametersRequest
): Observable<any> {
return this.httpClient.get(this.nifiCommon.stripProtocol(updateRequest.uri));
}
deleteParameterProviderParametersUpdateRequest(
updateRequest: ParameterProviderApplyParametersRequest
): Observable<any> {
return this.httpClient.delete(this.nifiCommon.stripProtocol(updateRequest.uri));
}
}

View File

@ -16,9 +16,11 @@
*/
import {
AffectedComponentEntity,
Bundle,
DocumentedType,
ParameterContextReferenceEntity,
ParameterEntity,
Permissions,
PropertyDescriptor,
Revision
@ -26,7 +28,31 @@ import {
export const parameterProvidersFeatureKey = 'parameterProviders';
export interface ParameterSensitivity {
name: string;
sensitive: boolean;
}
export interface ParameterGroupConfiguration {
groupName: string;
parameterContextName: string;
parameterSensitivities: { [key: string]: null | 'SENSITIVE' | 'NON_SENSITIVE' };
synchronized?: boolean;
}
export interface ParameterStatusEntity {
parameter?: ParameterEntity;
status: 'NEW' | 'CHANGED' | 'REMOVED' | 'MISSING_BUT_REFERENCED' | 'UNCHANGED';
}
export interface FetchedParameterMapping {
name: string;
sensitivity?: ParameterSensitivity;
status?: ParameterStatusEntity;
}
export interface ParameterProvider {
affectedComponents: AffectedComponentEntity[];
bundle: Bundle;
comments: string;
deprecated: boolean;
@ -35,7 +61,8 @@ export interface ParameterProvider {
id: string;
multipleVersionsAvailable: boolean;
name: string;
parameterGroupConfigurations: any[];
parameterGroupConfigurations: ParameterGroupConfiguration[];
parameterStatus?: ParameterStatusEntity[];
persistsState: boolean;
properties: { [key: string]: string };
referencingParameterContexts: ParameterContextReferenceEntity[];
@ -54,12 +81,46 @@ export interface ParameterProviderEntity {
uri: string;
}
export interface ParameterProviderParameterApplicationEntity {
id: string;
revision: Revision;
disconnectedNodeAcknowledged: boolean;
parameterGroupConfigurations: ParameterGroupConfiguration[];
}
export interface UpdateStep {
description: string;
complete: boolean;
failureReason?: string;
}
export interface ParameterContextUpdateRequest {
parameterContextRevision: Revision;
parameterContext: any;
referencingComponents: AffectedComponentEntity[];
}
// returned from '/apply-parameters-request'
export interface ParameterProviderApplyParametersRequest {
requestId: string;
complete: boolean;
lastUpdated: string;
percentComplete: number;
state: string;
uri: string;
parameterContextUpdates: ParameterContextUpdateRequest[];
parameterProvider: ParameterProvider;
referencingComponents: AffectedComponentEntity[];
updateSteps: UpdateStep[];
}
export interface ParameterProvidersState {
parameterProviders: ParameterProviderEntity[];
fetched: ParameterProviderEntity | null;
applyParametersRequestEntity: ParameterProviderApplyParametersRequest | null;
saving: boolean;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
status: 'pending' | 'loading' | 'success';
}
export interface LoadParameterProvidersResponse {
@ -115,3 +176,21 @@ export interface UpdateParameterProviderRequest {
payload: any;
postUpdateNavigation?: string[];
}
export interface FetchParameterProviderParametersRequest {
id: string;
revision: Revision;
}
export interface FetchParameterProviderParametersResponse {
parameterProvider: ParameterProviderEntity;
}
export interface FetchParameterProviderDialogRequest {
id: string;
parameterProvider: ParameterProviderEntity;
}
export interface PollParameterProviderParametersUpdateSuccess {
request: ParameterProviderApplyParametersRequest;
}

View File

@ -24,7 +24,11 @@ import {
DeleteParameterProviderRequest,
DeleteParameterProviderSuccess,
EditParameterProviderRequest,
FetchParameterProviderParametersRequest,
FetchParameterProviderParametersResponse,
LoadParameterProvidersResponse,
ParameterProviderParameterApplicationEntity,
PollParameterProviderParametersUpdateSuccess,
SelectParameterProviderRequest
} from './index';
@ -39,7 +43,7 @@ export const loadParameterProvidersSuccess = createAction(
props<{ response: LoadParameterProvidersResponse }>()
);
export const parameterProvidersApiError = createAction(
export const parameterProvidersBannerApiError = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Load Parameter Providers Error`,
props<{ error: string }>()
);
@ -83,6 +87,11 @@ export const navigateToEditParameterProvider = createAction(
props<{ id: string }>()
);
export const navigateToFetchParameterProvider = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Navigate To Fetch Parameter Provider`,
props<{ id: string }>()
);
export const openConfigureParameterProviderDialog = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Open Configure Parameter Provider Dialog`,
props<{ request: EditParameterProviderRequest }>()
@ -97,3 +106,58 @@ export const configureParameterProviderSuccess = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Configure Parameter Provider Success`,
props<{ response: ConfigureParameterProviderSuccess }>()
);
export const fetchParameterProviderParametersAndOpenDialog = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Fetch Parameter Provider Parameters and Open Dialog`,
props<{ request: FetchParameterProviderParametersRequest }>()
);
export const fetchParameterProviderParametersSuccess = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Fetch Parameter Provider Parameters Success`,
props<{ response: FetchParameterProviderParametersResponse }>()
);
export const openFetchParameterProviderDialog = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Open Fetch Parameter Provider Parameters Dialog`,
props<{ request: FetchParameterProviderParametersResponse }>()
);
export const resetFetchedParameterProvider = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Reset Fetched Parameter Provider`
);
// UPDATE FETCHED PARAMETERS
export const submitParameterProviderParametersUpdateRequest = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Submit Parameter Provider Parameters Update Request`,
props<{ request: ParameterProviderParameterApplicationEntity }>()
);
export const submitParameterProviderParametersUpdateRequestSuccess = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Submit Parameter Provider Parameters Update Request Success`,
props<{ response: PollParameterProviderParametersUpdateSuccess }>()
);
export const startPollingParameterProviderParametersUpdateRequest = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Start Polling Parameter Provider Parameters Update Request`
);
export const pollParameterProviderParametersUpdateRequest = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Poll Parameter Provider Parameters Update Request`
);
export const pollParameterProviderParametersUpdateRequestSuccess = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Poll Parameter Provider Parameters Update Request Success`,
props<{ response: PollParameterProviderParametersUpdateSuccess }>()
);
export const stopPollingParameterProviderParametersUpdateRequest = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Stop Polling Parameter Provider Parameters Update Request`
);
export const deleteParameterProviderParametersUpdateRequest = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Delete Parameter Provider Parameters Update Request`
);
export const submitParameterProviderParametersUpdateComplete = createAction(
`${PARAMETER_PROVIDERS_PREFIX} Submit Parameter Provider Parameters Update Complete`
);

View File

@ -24,16 +24,37 @@ import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { ParameterProviderService } from '../../service/parameter-provider.service';
import * as ParameterProviderActions from './parameter-providers.actions';
import { loadParameterProviders, selectParameterProvider } from './parameter-providers.actions';
import { catchError, from, map, of, switchMap, take, takeUntil, tap } from 'rxjs';
import { selectSaving } from './parameter-providers.selectors';
import { loadParameterProviders } from './parameter-providers.actions';
import {
asyncScheduler,
catchError,
filter,
from,
interval,
map,
NEVER,
of,
switchMap,
take,
takeUntil,
tap
} from 'rxjs';
import {
selectApplyParameterProviderParametersRequest,
selectSaving,
selectStatus
} from './parameter-providers.selectors';
import { selectParameterProviderTypes } from '../../../../state/extension-types/extension-types.selectors';
import { CreateParameterProvider } from '../../ui/parameter-providers/create-parameter-provider/create-parameter-provider.component';
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
import { EditParameterProvider } from '../../ui/parameter-providers/edit-parameter-provider/edit-parameter-provider.component';
import { PropertyTableHelperService } from '../../../../service/property-table-helper.service';
import { UpdateParameterProviderRequest } from './index';
import { ParameterProviderEntity, UpdateParameterProviderRequest } from './index';
import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
import { FetchParameterProviderParameters } from '../../ui/parameter-providers/fetch-parameter-provider-parameters/fetch-parameter-provider-parameters.component';
import * as ErrorActions from '../../../../state/error/error.actions';
import { HttpErrorResponse } from '@angular/common/http';
import { ErrorHelper } from '../../../../service/error-helper.service';
@Injectable()
export class ParameterProvidersEffects {
@ -45,13 +66,15 @@ export class ParameterProvidersEffects {
private router: Router,
private parameterProviderService: ParameterProviderService,
private propertyTableHelperService: PropertyTableHelperService,
private managementControllerServiceService: ManagementControllerServiceService
private managementControllerServiceService: ManagementControllerServiceService,
private errorHelper: ErrorHelper
) {}
loadParameterProviders$ = createEffect(() =>
this.actions$.pipe(
ofType(loadParameterProviders),
switchMap(() =>
concatLatestFrom(() => this.store.select(selectStatus)),
switchMap(([, status]) =>
from(this.parameterProviderService.getParameterProviders()).pipe(
map((response) =>
ParameterProviderActions.loadParameterProvidersSuccess({
@ -61,9 +84,7 @@ export class ParameterProvidersEffects {
}
})
),
catchError((error) =>
of(ParameterProviderActions.parameterProvidersApiError({ error: error.error }))
)
catchError((error: HttpErrorResponse) => of(this.errorHelper.handleLoadingError(status, error)))
)
)
)
@ -72,7 +93,7 @@ export class ParameterProvidersEffects {
selectParameterProvider$ = createEffect(
() =>
this.actions$.pipe(
ofType(selectParameterProvider),
ofType(ParameterProviderActions.selectParameterProvider),
map((action) => action.request),
tap((request) => {
this.router.navigate(['/settings', 'parameter-providers', request.id]);
@ -130,9 +151,10 @@ export class ParameterProvidersEffects {
}
})
),
catchError((error) =>
of(ParameterProviderActions.parameterProvidersApiError({ error: error.error }))
)
catchError((error: HttpErrorResponse) => {
this.dialog.closeAll();
return of(ErrorActions.snackBarError({ error: error.error }));
})
)
)
)
@ -196,13 +218,7 @@ export class ParameterProvidersEffects {
}
})
),
catchError((error) =>
of(
ParameterProviderActions.parameterProvidersApiError({
error: error.error
})
)
)
catchError((error) => of(ErrorActions.snackBarError({ error: error.error })))
)
)
)
@ -220,6 +236,18 @@ export class ParameterProvidersEffects {
{ dispatch: false }
);
navigateToFetchParameterProvider$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterProviderActions.navigateToFetchParameterProvider),
map((action) => action.id),
tap((id) => {
this.router.navigate(['settings', 'parameter-providers', id, 'fetch']);
})
),
{ dispatch: false }
);
openConfigureParameterProviderDialog$ = createEffect(
() =>
this.actions$.pipe(
@ -298,6 +326,8 @@ export class ParameterProvidersEffects {
});
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(ErrorActions.clearBannerErrors());
if (response !== 'ROUTED') {
this.store.dispatch(
ParameterProviderActions.selectParameterProvider({
@ -328,13 +358,18 @@ export class ParameterProvidersEffects {
}
})
),
catchError((error) =>
of(
ParameterProviderActions.parameterProvidersApiError({
error: error.error
})
)
)
catchError((error: HttpErrorResponse) => {
if (this.errorHelper.showErrorInContext(error.status)) {
return of(
ParameterProviderActions.parameterProvidersBannerApiError({
error: error.error
})
);
} else {
this.dialog.getDialogById(request.id)?.close('ROUTED');
return of(this.errorHelper.fullScreenError(error));
}
})
)
)
)
@ -356,4 +391,246 @@ export class ParameterProvidersEffects {
),
{ dispatch: false }
);
fetchParameterProviderParametersAndOpenDialog$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.fetchParameterProviderParametersAndOpenDialog),
map((action) => action.request),
switchMap((request) =>
from(this.parameterProviderService.fetchParameters(request)).pipe(
map((response: ParameterProviderEntity) =>
ParameterProviderActions.fetchParameterProviderParametersSuccess({
response: { parameterProvider: response }
})
),
catchError((error) => {
if (this.errorHelper.showErrorInContext(error.status)) {
this.store.dispatch(
ParameterProviderActions.selectParameterProvider({
request: {
id: request.id
}
})
);
return of(ErrorActions.snackBarError({ error: error.error }));
} else {
return of(ErrorActions.fullScreenError(error));
}
})
)
)
)
);
fetchParameterProviderParametersSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.fetchParameterProviderParametersSuccess),
map((action) => action.response),
switchMap((response) =>
of(ParameterProviderActions.openFetchParameterProviderDialog({ request: response }))
)
)
);
openFetchParameterProvidersParametersDialog$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterProviderActions.openFetchParameterProviderDialog),
map((action) => action.request),
tap((request) => {
const dialogRef = this.dialog.open(FetchParameterProviderParameters, {
panelClass: 'xl-dialog',
data: request
});
const referencingParameterContexts =
request.parameterProvider.component.referencingParameterContexts;
if (referencingParameterContexts?.length > 0) {
// add an error if one of the referenced parameter contexts is not readable/writeable
const canReadWriteAll = referencingParameterContexts.every(
(paramContextRef) =>
paramContextRef.permissions.canRead && paramContextRef.permissions.canWrite
);
if (!canReadWriteAll) {
this.store.dispatch(
ParameterProviderActions.parameterProvidersBannerApiError({
error: 'You do not have permissions to modify one or more synced parameter contexts.'
})
);
}
}
const affectedComponents = request.parameterProvider.component.affectedComponents;
if (affectedComponents?.length > 0) {
// add an error if one of the affected components is not readable/writeable
const canReadWriteAll = affectedComponents.every(
(paramContextRef) =>
paramContextRef.permissions.canRead && paramContextRef.permissions.canWrite
);
if (!canReadWriteAll) {
this.store.dispatch(
ParameterProviderActions.parameterProvidersBannerApiError({
error: 'You do not have permissions to modify one or more affected components.'
})
);
}
}
dialogRef.componentInstance.updateRequest = this.store.select(
selectApplyParameterProviderParametersRequest
);
dialogRef.componentInstance.saving$ = this.store.select(selectSaving);
dialogRef.afterClosed().subscribe((response) => {
this.store.dispatch(ParameterProviderActions.resetFetchedParameterProvider());
this.store.dispatch(ErrorActions.clearBannerErrors());
if (response !== 'ROUTED') {
this.store.dispatch(
ParameterProviderActions.selectParameterProvider({
request: {
id: request.parameterProvider.id
}
})
);
this.store.dispatch(
ParameterProviderActions.submitParameterProviderParametersUpdateComplete()
);
}
});
})
),
{ dispatch: false }
);
parameterProvidersBannerApiError$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.parameterProvidersBannerApiError),
map((action) => action.error),
tap(() =>
this.store.dispatch(ParameterProviderActions.stopPollingParameterProviderParametersUpdateRequest())
),
switchMap((error) => of(ErrorActions.addBannerError({ error })))
)
);
submitParameterProviderParametersUpdateRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.submitParameterProviderParametersUpdateRequest),
map((action) => action.request),
switchMap((request) =>
from(
this.parameterProviderService.applyParameters(request).pipe(
map((response: any) =>
ParameterProviderActions.submitParameterProviderParametersUpdateRequestSuccess({
response: {
request: response.request
}
})
),
catchError((error) =>
of(
ParameterProviderActions.parameterProvidersBannerApiError({
error: error.error
})
)
)
)
)
)
)
);
submitParameterProviderParametersUpdateRequestSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.submitParameterProviderParametersUpdateRequestSuccess),
map((action) => action.response),
switchMap((response) => {
const updateRequest = response.request;
if (updateRequest.complete) {
return of(ParameterProviderActions.deleteParameterProviderParametersUpdateRequest());
} else {
return of(ParameterProviderActions.startPollingParameterProviderParametersUpdateRequest());
}
})
)
);
startPollingParameterProviderParametersUpdateRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.startPollingParameterProviderParametersUpdateRequest),
switchMap(() =>
interval(2000, asyncScheduler).pipe(
takeUntil(
this.actions$.pipe(
ofType(ParameterProviderActions.stopPollingParameterProviderParametersUpdateRequest)
)
)
)
),
switchMap(() => of(ParameterProviderActions.pollParameterProviderParametersUpdateRequest()))
)
);
pollParameterProviderParametersUpdateRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.pollParameterProviderParametersUpdateRequest),
concatLatestFrom(() => this.store.select(selectApplyParameterProviderParametersRequest)),
switchMap(([, updateRequest]) => {
if (updateRequest) {
return from(
this.parameterProviderService.pollParameterProviderParametersUpdateRequest(updateRequest)
).pipe(
map((response) =>
ParameterProviderActions.pollParameterProviderParametersUpdateRequestSuccess({
response: {
request: response.request
}
})
),
catchError((error) =>
of(
ParameterProviderActions.parameterProvidersBannerApiError({
error: error.error
})
)
)
);
} else {
return NEVER;
}
})
)
);
pollParameterProviderParametersUpdateRequestSuccess$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.pollParameterProviderParametersUpdateRequestSuccess),
map((action) => action.response),
filter((response) => response.request.complete),
switchMap(() => {
return of(ParameterProviderActions.stopPollingParameterProviderParametersUpdateRequest());
})
)
);
stopPollingParameterProviderParametersUpdateRequest$ = createEffect(() =>
this.actions$.pipe(
ofType(ParameterProviderActions.stopPollingParameterProviderParametersUpdateRequest),
switchMap(() => of(ParameterProviderActions.deleteParameterProviderParametersUpdateRequest()))
)
);
deleteParameterProviderParametersUpdateRequest$ = createEffect(
() =>
this.actions$.pipe(
ofType(ParameterProviderActions.deleteParameterProviderParametersUpdateRequest),
concatLatestFrom(() => this.store.select(selectApplyParameterProviderParametersRequest)),
tap(([, updateRequest]) => {
if (updateRequest) {
this.parameterProviderService.deleteParameterProviderParametersUpdateRequest(updateRequest);
}
})
),
{ dispatch: false }
);
}

View File

@ -23,19 +23,26 @@ import {
createParameterProvider,
createParameterProviderSuccess,
deleteParameterProvider,
deleteParameterProviderParametersUpdateRequest,
deleteParameterProviderSuccess,
fetchParameterProviderParametersSuccess,
loadParameterProviders,
loadParameterProvidersSuccess,
parameterProvidersApiError,
resetParameterProvidersState
parameterProvidersBannerApiError,
pollParameterProviderParametersUpdateRequestSuccess,
resetFetchedParameterProvider,
resetParameterProvidersState,
submitParameterProviderParametersUpdateRequest,
submitParameterProviderParametersUpdateRequestSuccess
} from './parameter-providers.actions';
import { produce } from 'immer';
export const initialParameterProvidersState: ParameterProvidersState = {
parameterProviders: [],
fetched: null,
applyParametersRequestEntity: null,
saving: false,
loadedTimestamp: '',
error: null,
status: 'pending'
};
@ -55,15 +62,12 @@ export const parameterProvidersReducer = createReducer(
...state,
parameterProviders: response.parameterProviders,
loadedTimestamp: response.loadedTimestamp,
error: null,
status: 'success' as const
})),
on(parameterProvidersApiError, (state: ParameterProvidersState, { error }) => ({
on(parameterProvidersBannerApiError, (state: ParameterProvidersState) => ({
...state,
saving: false,
error,
status: 'error' as const
saving: false
})),
on(createParameterProvider, configureParameterProvider, deleteParameterProvider, (state) => ({
@ -102,5 +106,39 @@ export const parameterProvidersReducer = createReducer(
draftState.saving = false;
});
})
}),
on(fetchParameterProviderParametersSuccess, (state, { response }) => {
return {
...state,
fetched: response.parameterProvider
};
}),
on(resetFetchedParameterProvider, (state) => {
return {
...state,
fetched: null,
applyParametersRequestEntity: null
};
}),
on(submitParameterProviderParametersUpdateRequest, (state) => ({
...state,
saving: true
})),
on(
submitParameterProviderParametersUpdateRequestSuccess,
pollParameterProviderParametersUpdateRequestSuccess,
(state, { response }) => ({
...state,
applyParametersRequestEntity: response.request
})
),
on(deleteParameterProviderParametersUpdateRequest, (state) => ({
...state,
saving: false
}))
);

View File

@ -30,6 +30,11 @@ export const selectSaving = createSelector(
(state: ParameterProvidersState) => state.saving
);
export const selectStatus = createSelector(
selectParameterProvidersState,
(state: ParameterProvidersState) => state.status
);
export const selectParameterProviderIdFromRoute = createSelector(selectCurrentRoute, (route) => {
if (route) {
// always select the parameter provider from the route
@ -45,6 +50,13 @@ export const selectSingleEditedParameterProvider = createSelector(selectCurrentR
return null;
});
export const selectSingleFetchParameterProvider = createSelector(selectCurrentRoute, (route) => {
if (route?.routeConfig?.path == 'fetch') {
return route.params.id;
}
return null;
});
export const selectParameterProviders = createSelector(
selectParameterProvidersState,
(state: ParameterProvidersState) => state.parameterProviders
@ -52,5 +64,10 @@ export const selectParameterProviders = createSelector(
export const selectParameterProvider = (id: string) =>
createSelector(selectParameterProviders, (entities: ParameterProviderEntity[]) =>
entities.find((entity) => id == entity.id)
entities.find((entity) => id === entity.id)
);
export const selectApplyParameterProviderParametersRequest = createSelector(
selectParameterProvidersState,
(state: ParameterProvidersState) => state.applyParametersRequestEntity
);

View File

@ -17,6 +17,7 @@
<h2 mat-dialog-title>Edit Parameter Provider</h2>
<form class="parameter-provider-edit-form" [formGroup]="editParameterProviderForm">
<error-banner></error-banner>
<mat-dialog-content>
<mat-tab-group>
<mat-tab label="Settings">
@ -78,7 +79,7 @@
</mat-dialog-content>
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as saving">
<button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
<button color="primary" mat-stroked-button mat-dialog-close>Cancel</button>
<button
[disabled]="!editParameterProviderForm.dirty || editParameterProviderForm.invalid || saving.value"
type="button"

View File

@ -21,6 +21,8 @@ import { EditParameterProvider } from './edit-parameter-provider.component';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { EditParameterProviderRequest } from '../../../state/parameter-providers';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { provideMockStore } from '@ngrx/store/testing';
import { initialParameterProvidersState } from '../../../state/parameter-providers/parameter-providers.reducer';
describe('EditParameterProvider', () => {
let component: EditParameterProvider;
@ -58,6 +60,7 @@ describe('EditParameterProvider', () => {
'parameter-value-byte-limit': '256 B',
'parameter-value-encoding': 'plaintext'
},
affectedComponents: [],
descriptors: {
'parameter-group-directories': {
name: 'parameter-group-directories',
@ -156,7 +159,10 @@ describe('EditParameterProvider', () => {
{
provide: MAT_DIALOG_DATA,
useValue: data
}
},
provideMockStore({
initialState: initialParameterProvidersState
})
]
});
fixture = TestBed.createComponent(EditParameterProvider);

View File

@ -41,6 +41,7 @@ import { MatInputModule } from '@angular/material/input';
import { ControllerServiceReferences } from '../../../../../ui/common/controller-service/controller-service-references/controller-service-references.component';
import { ParameterProviderReferences } from '../parameter-context-references/parameter-provider-references.component';
import { PropertyTable } from '../../../../../ui/common/property-table/property-table.component';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
@Component({
selector: 'edit-parameter-provider',
@ -56,7 +57,8 @@ import { PropertyTable } from '../../../../../ui/common/property-table/property-
MatInputModule,
ControllerServiceReferences,
ParameterProviderReferences,
PropertyTable
PropertyTable,
ErrorBanner
],
templateUrl: './edit-parameter-provider.component.html',
styleUrls: ['./edit-parameter-provider.component.scss']

View File

@ -0,0 +1,342 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<div tabindex="0">
<h2 mat-dialog-title>Fetch Parameters</h2>
<form class="parameter-provider-fetch-form" [formGroup]="fetchParametersForm">
<error-banner></error-banner>
<mat-dialog-content *ngIf="(updateRequest | async)! as requestEntity; else fetchFormContent">
<div class="dialog-content flex gap-x-4 h-full w-full pt-2">
<div class="flex flex-col flex-1">
<div class="flex flex-col mb-4">
<div>Name</div>
<div class="value">{{ parameterProvider.component.name }}</div>
</div>
<div class="flex flex-col mb-4">
<div>Parameter Groups</div>
<div class="value">
{{ parameterGroupNames | sort | join }}
</div>
</div>
</div>
<div class="flex flex-col flex-1">
<div class="flex flex-col mb-4">
<div>Steps To Update Parameters</div>
<div class="flex flex-col gap-y-1.5">
<div
*ngFor="let updateStep of requestEntity.updateSteps"
class="flex justify-between items-center">
<div class="value">{{ updateStep.description }}</div>
<div
*ngIf="updateStep.complete; else stepInProgress"
class="fa fa-check complete"></div>
<ng-template #stepInProgress>
<div class="fa fa-spin fa-circle-o-notch"></div>
</ng-template>
</div>
</div>
</div>
</div>
<div class="flex flex-1">
<div class="flex flex-col flex-1">
<div class="flex flex-col mb-4">
<div class="flex flex-row items-center gap-x-2">
Parameter Contexts To Create
<i
class="fa fa-question-circle"
title="Parameter groups set to be created as parameter contexts, pending apply."></i>
</div>
<div *ngIf="Object.keys(parameterContextsToCreate).length > 0; else none" class="value">
{{ Object.values(parameterContextsToCreate) | sort | join }}
</div>
</div>
<div class="flex flex-col mb-4">
<div class="flex flex-row items-center gap-x-2">
Parameter Contexts To Update
<i
class="fa fa-question-circle"
title="Synced parameter contexts to be updated, pending apply."></i>
</div>
<div *ngIf="parameterContextsToUpdate.length > 0; else none" class="value">
{{ parameterContextsToUpdate | sort | join }}
</div>
</div>
<div class="flex flex-1 flex-col">
<div class="flex flex-row items-center gap-x-2">
Affected Referencing Components
<i
class="fa fa-question-circle"
title="Affected components referencing this parameter provider."></i>
</div>
<div class="relative h-full border">
<div class="absolute inset-0 overflow-y-auto p-1">
<parameter-references
[parameterReferences]="
parameterProvider.component.affectedComponents
"></parameter-references>
</div>
</div>
</div>
<ng-template #none>
<div class="unset">None</div>
</ng-template>
</div>
</div>
</div>
</mat-dialog-content>
<ng-template #fetchFormContent>
<mat-dialog-content>
<div class="dialog-content flex gap-x-4 h-full w-full pt-2">
<div class="flex flex-col flex-1">
<div class="flex flex-col mb-4">
<div>Name</div>
<div class="value">{{ parameterProvider.component.name }}</div>
</div>
<div class="flex flex-col flex-1">
<div class="flex flex-row items-center gap-x-2">
Select To Configure a Parameter Group
<i
class="fa fa-question-circle"
title="Discovered parameter groups from this parameter provider. Select a group to create a parameter context, then configure its parameter sensitivities."></i>
</div>
<div class="flex-1">
<parameter-groups-table
[parameterGroups]="parameterGroupConfigurations"
(selected)="parameterGroupSelected($event)"></parameter-groups-table>
</div>
</div>
</div>
<div class="flex flex-1">
<ng-container *ngFor="let parameterGroupConfig of parameterGroupConfigurations">
<!-- Only show the parameters associated with the selected group -->
<div
[formGroupName]="parameterGroupConfig.groupName"
[ngClass]="{
hidden: parameterGroupConfig.groupName !== selectedParameterGroup?.groupName
}"
class="flex gap-y-4 h-full w-full flex-col">
<ng-container
*ngIf="canCreateParameterContext(parameterGroupConfig); else paramContextSynced">
<!-- Not synced, give the user the option to create a parameter context for the group -->
<div class="flex items-center">
<mat-checkbox
color="primary"
formControlName="createParameterContext"
(change)="createParameterContextToggled($event)">
<mat-label>Create Parameter Context</mat-label>
</mat-checkbox>
</div>
<div
class="flex flex-col"
*ngIf="canEditParameterContextName(parameterGroupConfig)">
<mat-form-field>
<mat-label>Parameter Context Name</mat-label>
<input
matInput
type="text"
formControlName="parameterContextName"
[value]="parameterGroupConfig.parameterContextName" />
</mat-form-field>
</div>
</ng-container>
<!-- If the group is synchronized, show the parameter context name in read-only mode -->
<ng-template #paramContextSynced>
<div class="flex flex-col">
<div>Parameter Context Name</div>
<div class="value">{{ parameterGroupConfig.parameterContextName }}</div>
</div>
</ng-template>
<ng-container *ngIf="showParameterList(parameterGroupConfig); else parameterMapping">
<!-- Show the parameters defined -->
<div class="flex flex-1 flex-col overflow-hidden">
<div class="flex items-center gap-x-2">
Fetched Parameters
<i
class="fa fa-question-circle"
title="Discovered parameters from the selected parameter group."></i>
</div>
<ul class="flex-1 overflow-y-auto border px-2">
<li
*ngFor="
let param of Object.entries(
parameterGroupConfig.parameterSensitivities
)
"
class="value">
{{ param[0] }}
</li>
</ul>
</div>
</ng-container>
<ng-template #parameterMapping>
<div class="flex flex-1 flex-col">
<div class="flex flex-row items-center gap-x-2">
Select Parameters To Be Set As Sensitive
<i
class="fa fa-question-circle"
title="Only parameters that are not referenced can be modified."></i>
</div>
<div class="flex-1 relative">
<div class="listing-table overflow-y-auto border absolute inset-0">
<table
mat-table
[dataSource]="getParameterMappingDataSource(parameterGroupConfig)"
matSort
matSortDisableClear
matSortActive="name"
matSortDirection="asc"
(matSortChange)="sort($event)">
<ng-container matColumnDef="sensitive">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox
color="primary"
[checked]="areAllSelected(parameterGroupConfig)"
[indeterminate]="areAnySelected(parameterGroupConfig)"
(change)="selectAllChanged($event)"></mat-checkbox>
</th>
<td mat-cell *matCellDef="let item" class="items-center">
<mat-checkbox
color="primary"
[formControl]="
getFormControl(item, parameterGroupConfig)
"
[checked]="item.sensitivity.sensitive"></mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>
Parameter Name
</th>
<td mat-cell *matCellDef="let item" class="items-center">
<div>{{ item.name }}</div>
</td>
</ng-container>
<ng-container matColumnDef="indicators">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center gap-x-3 justify-end">
<div
class="fa fa-hashtag"
title="Parameter is currently referenced by a property. The sensitivity cannot be changed."
*ngIf="isReferenced(item)"></div>
<div
class="fa fa-asterisk"
[title]="getAffectedTooltip(item)"
*ngIf="isAffected(item)"></div>
</div>
</td>
</ng-container>
<tr
mat-header-row
*matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr
mat-row
*matRowDef="let row; let even = even; columns: displayedColumns"
(click)="selectParameter(row)"
[class.selected]="isParameterSelected(row)"
[class.even]="even"></tr>
</table>
</div>
</div>
</div>
</ng-template>
</div>
</ng-container>
</div>
<div class="flex flex-1">
<div class="flex flex-col flex-1">
<div class="flex flex-col mb-4">
<div class="flex flex-row items-center gap-x-2">
Parameter Contexts To Create
<i
class="fa fa-question-circle"
title="Parameter groups set to be created as parameter contexts, pending apply."></i>
</div>
<div *ngIf="Object.keys(parameterContextsToCreate).length > 0; else none" class="value">
{{ Object.values(parameterContextsToCreate) | sort | join }}
</div>
</div>
<div class="flex flex-col mb-4">
<div class="flex flex-row items-center gap-x-2">
Parameter Contexts To Update
<i
class="fa fa-question-circle"
title="Synced parameter contexts to be updated, pending apply."></i>
</div>
<div *ngIf="parameterContextsToUpdate.length > 0; else none" class="value">
{{ parameterContextsToUpdate | sort | join }}
</div>
</div>
<div class="flex flex-1 flex-col">
<div class="flex flex-row items-center gap-x-2">
Referencing Components
<i
class="fa fa-question-circle"
title="Components referencing this selected parameter."></i>
</div>
<div class="relative h-full border">
<div class="absolute inset-0 overflow-y-auto p-1">
<parameter-references
[parameterReferences]="parameterReferences"></parameter-references>
</div>
</div>
</div>
<ng-template #none>
<div class="unset">None</div>
</ng-template>
</div>
</div>
</div>
</mat-dialog-content>
</ng-template>
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as saving">
<ng-container *ngIf="updateRequest | async; else normalActions">
<!-- an update to the associated parameter context(s) has been triggered -->
<button color="primary" mat-stroked-button mat-dialog-close>
<span *nifiSpinner="saving.value">Close</span>
</button>
</ng-container>
<ng-template #normalActions>
<button color="primary" mat-stroked-button mat-dialog-close>Cancel</button>
<button
[disabled]="!canSubmitForm() || saving.value"
type="button"
color="primary"
(click)="submitForm()"
mat-raised-button>
<span *nifiSpinner="saving.value">Apply</span>
</button>
</ng-template>
</mat-dialog-actions>
</form>
</div>

View File

@ -0,0 +1,47 @@
/*!
* 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;
.parameter-provider-fetch-form {
@include mat.button-density(-1);
.mdc-dialog__content {
padding: 0 16px;
font-size: 14px;
.dialog-content {
height: 475px;
overflow-y: auto;
}
}
.mat-mdc-form-field {
width: 100%;
}
.listing-table {
.mat-column-sensitive {
width: 32px;
min-width: 32px;
}
.mat-column-indicators {
width: 48px;
min-width: 48px;
}
}
}

View File

@ -0,0 +1,177 @@
/*
* 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 { FetchParameterProviderParameters } from './fetch-parameter-provider-parameters.component';
import { FetchParameterProviderDialogRequest } from '../../../state/parameter-providers';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { provideMockStore } from '@ngrx/store/testing';
import { initialParameterProvidersState } from '../../../state/parameter-providers/parameter-providers.reducer';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('FetchParameterProviderParameters', () => {
let component: FetchParameterProviderParameters;
let fixture: ComponentFixture<FetchParameterProviderParameters>;
const data: FetchParameterProviderDialogRequest = {
id: 'id',
parameterProvider: {
revision: {
clientId: '36ba1cc1-018d-1000-bc2c-787bc552d63d',
version: 6
},
id: '369487d7-018d-1000-817a-1d8d9a8f4a91',
uri: 'https://localhost:8443/nifi-api/parameter-providers/369487d7-018d-1000-817a-1d8d9a8f4a91',
permissions: {
canRead: true,
canWrite: true
},
bulletins: [],
component: {
id: '369487d7-018d-1000-817a-1d8d9a8f4a91',
name: 'Group 1 - FileParameterProvider',
type: 'org.apache.nifi.parameter.FileParameterProvider',
bundle: {
group: 'org.apache.nifi',
artifact: 'nifi-standard-nar',
version: '2.0.0-SNAPSHOT'
},
comments: '',
persistsState: false,
restricted: true,
deprecated: false,
multipleVersionsAvailable: false,
properties: {
'parameter-group-directories': '/Users/rfellows/tmp/parameterProviders/group1',
'parameter-value-byte-limit': '256 B',
'parameter-value-encoding': 'plaintext'
},
affectedComponents: [],
descriptors: {
'parameter-group-directories': {
name: 'parameter-group-directories',
displayName: 'Parameter Group Directories',
description:
'A comma-separated list of directory absolute paths that will map to named parameter groups. Each directory that contains files will map to a parameter group, named after the innermost directory in the path. Files inside the directory will map to parameter names, whose values are the content of each respective file.',
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
'parameter-value-byte-limit': {
name: 'parameter-value-byte-limit',
displayName: 'Parameter Value Byte Limit',
description:
'The maximum byte size of a parameter value. Since parameter values are pulled from the contents of files, this is a safeguard that can prevent memory issues if large files are included.',
defaultValue: '256 B',
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
},
'parameter-value-encoding': {
name: 'parameter-value-encoding',
displayName: 'Parameter Value Encoding',
description: 'Indicates how parameter values are encoded inside Parameter files.',
defaultValue: 'base64',
allowableValues: [
{
allowableValue: {
displayName: 'Base64',
value: 'base64',
description:
'File content is Base64-encoded, and will be decoded before providing the value as a Parameter.'
},
canRead: true
},
{
allowableValue: {
displayName: 'Plain text',
value: 'plaintext',
description:
'File content is not encoded, and will be provided directly as a Parameter value.'
},
canRead: true
}
],
required: true,
sensitive: false,
dynamic: false,
supportsEl: false,
expressionLanguageScope: 'Not Supported',
dependencies: []
}
},
parameterGroupConfigurations: [
{
groupName: 'group1',
parameterContextName: 'group1',
parameterSensitivities: {
bytes: 'NON_SENSITIVE',
password: 'SENSITIVE',
username: 'NON_SENSITIVE'
},
synchronized: true
}
],
referencingParameterContexts: [
{
id: '3716e18d-018d-1000-f203-4f6d571d572e',
permissions: {
canRead: true,
canWrite: true
},
bulletins: [],
component: {
id: '3716e18d-018d-1000-f203-4f6d571d572e',
name: 'group1'
}
}
],
validationStatus: 'VALID',
extensionMissing: false
}
}
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FetchParameterProviderParameters, NoopAnimationsModule],
providers: [
{
provide: MAT_DIALOG_DATA,
useValue: data
},
provideMockStore({
initialState: initialParameterProvidersState
})
]
});
fixture = TestBed.createComponent(FetchParameterProviderParameters);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,603 @@
/*
* 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, DestroyRef, inject, Inject, Input, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { MatButtonModule } from '@angular/material/button';
import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
import { Client } from '../../../../../service/client.service';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import {
FetchedParameterMapping,
FetchParameterProviderDialogRequest,
ParameterGroupConfiguration,
ParameterProviderApplyParametersRequest,
ParameterProviderEntity,
ParameterProviderParameterApplicationEntity,
ParameterProvidersState,
ParameterSensitivity,
ParameterStatusEntity
} from '../../../state/parameter-providers';
import { debounceTime, Observable, Subject } from 'rxjs';
import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
import { ParameterGroupsTable } from './parameter-groups-table/parameter-groups-table.component';
import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox';
import { MatInputModule } from '@angular/material/input';
import { ParameterReferences } from '../../../../../ui/common/parameter-references/parameter-references.component';
import { AffectedComponentEntity } from '../../../../../state/shared';
import * as ParameterProviderActions from '../../../state/parameter-providers/parameter-providers.actions';
import { Store } from '@ngrx/store';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { PipesModule } from '../../../../../pipes/pipes.module';
@Component({
selector: 'fetch-parameter-provider-parameters',
standalone: true,
imports: [
CommonModule,
MatDialogModule,
ReactiveFormsModule,
ErrorBanner,
MatButtonModule,
NifiSpinnerDirective,
NifiTooltipDirective,
MatTableModule,
MatSortModule,
ParameterGroupsTable,
MatCheckboxModule,
MatInputModule,
ParameterReferences,
PipesModule
],
templateUrl: './fetch-parameter-provider-parameters.component.html',
styleUrls: ['./fetch-parameter-provider-parameters.component.scss']
})
export class FetchParameterProviderParameters implements OnInit {
fetchParametersForm: FormGroup;
parameterProvider: ParameterProviderEntity;
selectedParameterGroup: ParameterGroupConfiguration | null = null;
parameterGroupConfigurations: ParameterGroupConfiguration[];
parameterGroupNames: string[] = [];
parameterContextsToCreate: { [key: string]: string } = {};
parameterContextsToUpdate: string[] = [];
displayedColumns = ['sensitive', 'name', 'indicators'];
// each group has a different set of parameters, map by groupName
dataSources: { [key: string]: MatTableDataSource<FetchedParameterMapping> } = {};
// each group's parameter table can have a different sort active, map by groupName
activeSorts: { [key: string]: Sort } = {};
// each group can have a selected parameter, map by groupName
selectedParameters: { [key: string]: FetchedParameterMapping } = {};
// as the selected parameter of the current group changes
selectParameterChanged: Subject<FetchedParameterMapping | null> = new Subject<FetchedParameterMapping | null>();
parameterReferences: AffectedComponentEntity[] = [];
@Input() saving$!: Observable<boolean>;
@Input() updateRequest!: Observable<ParameterProviderApplyParametersRequest | null>;
protected readonly TextTip = TextTip;
protected readonly Object = Object;
private destroyRef: DestroyRef = inject(DestroyRef);
constructor(
private formBuilder: FormBuilder,
private client: Client,
private nifiCommon: NiFiCommon,
private store: Store<ParameterProvidersState>,
@Inject(MAT_DIALOG_DATA) public request: FetchParameterProviderDialogRequest
) {
this.parameterProvider = request.parameterProvider;
this.fetchParametersForm = this.formBuilder.group({});
this.parameterGroupConfigurations = this.parameterProvider.component.parameterGroupConfigurations.slice();
this.parameterGroupConfigurations.forEach((parameterGroupConfig) => {
const params = this.getParameterSensitivitiesAsFormControls(parameterGroupConfig);
this.fetchParametersForm.addControl(
parameterGroupConfig.groupName,
this.formBuilder.group({
createParameterContext: new FormControl(),
parameterContextName: new FormControl(
parameterGroupConfig.parameterContextName,
Validators.required
),
parameterSensitivities: this.formBuilder.group(params)
})
);
this.parameterGroupNames.push(parameterGroupConfig.groupName);
});
if (this.parameterProvider.component.referencingParameterContexts) {
this.parameterContextsToUpdate = this.parameterProvider.component.referencingParameterContexts
.map((parameterContext) => parameterContext.component?.name ?? '')
.filter((name) => name.length > 0);
}
}
ngOnInit(): void {
this.selectParameterChanged.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((selectedParameter) => {
const parameterGroupName = this.selectedParameterGroup?.groupName;
if (selectedParameter) {
// keep track of the currently selected parameter for each group
if (parameterGroupName) {
this.selectedParameters[parameterGroupName] = selectedParameter;
this.parameterReferences =
selectedParameter.status?.parameter?.parameter.referencingComponents ?? [];
}
} else {
if (parameterGroupName) {
delete this.selectedParameters[parameterGroupName];
this.parameterReferences = [];
}
}
});
if (this.parameterGroupConfigurations.length > 0) {
// select the first parameter group
const initialParamGroup = this.parameterGroupConfigurations[0];
// preload the first datasource into the map
this.getParameterMappingDataSource(initialParamGroup);
this.autoSelectParameter();
// watch for changes to the parameter context name inputs, update the local map
this.parameterGroupConfigurations.forEach((groupConfig) => {
this.fetchParametersForm
.get(`${groupConfig.groupName}.parameterContextName`)
?.valueChanges.pipe(debounceTime(200), takeUntilDestroyed(this.destroyRef))
.subscribe((name) => {
if (Object.hasOwn(this.parameterContextsToCreate, groupConfig.groupName)) {
this.parameterContextsToCreate[groupConfig.groupName] = name;
}
});
});
}
}
submitForm() {
const data = this.getFormData();
this.store.dispatch(
ParameterProviderActions.submitParameterProviderParametersUpdateRequest({
request: data
})
);
}
parameterGroupSelected(parameterGroup: ParameterGroupConfiguration) {
this.selectedParameterGroup = parameterGroup;
this.autoSelectParameter();
}
private autoSelectParameter() {
if (this.selectedParameterGroup) {
const selectedParam = this.selectedParameters[this.selectedParameterGroup.groupName];
if (selectedParam) {
this.selectParameterChanged.next(selectedParam);
} else {
// select the first param
const paramsDs = this.dataSources[this.selectedParameterGroup.groupName];
if (paramsDs) {
this.selectParameterChanged.next(paramsDs.data[0]);
}
}
}
}
canCreateParameterContext(parameterGroupConfig: ParameterGroupConfiguration): boolean {
// the passed in parameter group could have changed its sync status due to user input,
// check the original parameter group from the server.
const originalParameterGroupConfig = this.parameterProvider.component.parameterGroupConfigurations.find(
(g) => g.groupName === parameterGroupConfig.groupName
);
if (originalParameterGroupConfig) {
return !this.isSynced(originalParameterGroupConfig);
}
return false;
}
canEditParameterContextName(parameterGroupConfig: ParameterGroupConfiguration): boolean {
// can only edit the context name if the create parameter context checkbox is checked.
return this.fetchParametersForm.get(`${parameterGroupConfig.groupName}.createParameterContext`)?.value;
}
showParameterList(parameterGroupConfig: ParameterGroupConfiguration): boolean {
// show only a list of parameters if the group is not synced with a parameter context and the user isn't actively trying to create a context for it
return (
!this.isSynced(parameterGroupConfig) &&
!this.fetchParametersForm.get(`${parameterGroupConfig.groupName}.createParameterContext`)?.value
);
}
isSynced(parameterGroupConfig: ParameterGroupConfiguration): boolean {
return !!parameterGroupConfig.synchronized;
}
getParameterMappingDataSource(parameterGroupConfig: ParameterGroupConfiguration) {
if (!this.dataSources[parameterGroupConfig.groupName]) {
const ds = new MatTableDataSource<FetchedParameterMapping>();
ds.data = this.sortEntities(
this.parameterMappingArray(parameterGroupConfig),
this.getActiveSort(parameterGroupConfig)
);
this.dataSources[parameterGroupConfig.groupName] = ds;
}
return this.dataSources[parameterGroupConfig.groupName];
}
getActiveSort(parameterGroupConfig: ParameterGroupConfiguration): Sort {
if (!this.activeSorts[parameterGroupConfig.groupName]) {
this.activeSorts[parameterGroupConfig.groupName] = {
active: 'name',
direction: 'asc'
};
}
return this.activeSorts[parameterGroupConfig.groupName];
}
setActiveSort(sort: Sort, parameterGroupConfig: ParameterGroupConfiguration) {
this.activeSorts[parameterGroupConfig.groupName] = sort;
}
getFormControl(parameter: ParameterSensitivity, parameterGroupConfig: ParameterGroupConfiguration): FormControl {
return this.fetchParametersForm.get(
`${parameterGroupConfig.groupName}.parameterSensitivities.${parameter.name}`
) as FormControl;
}
private getParameterMapping(parameterGroupConfig: ParameterGroupConfiguration): {
[key: string]: FetchedParameterMapping;
} {
const map: { [key: string]: FetchedParameterMapping } = {};
// get all the parameter status for the selected group, add them to the map by param name
if (this.parameterProvider.component.parameterStatus) {
this.parameterProvider.component.parameterStatus
.filter((parameterStatus: ParameterStatusEntity) => {
if (!parameterStatus?.parameter?.parameter) {
return false;
}
const param = parameterStatus.parameter.parameter;
return param.parameterContext?.component?.name === parameterGroupConfig.parameterContextName;
})
.forEach((parameterStatus: ParameterStatusEntity) => {
if (parameterStatus.parameter) {
const parameterName = parameterStatus.parameter.parameter.name;
map[parameterName] = {
name: parameterName,
status: parameterStatus,
sensitivity: {
name: parameterName,
sensitive: parameterStatus.parameter.parameter.sensitive
}
} as FetchedParameterMapping;
}
});
}
// get all the known parameter sensitivities, add them to the map by param name
if (parameterGroupConfig.parameterSensitivities) {
Object.entries(parameterGroupConfig.parameterSensitivities).forEach((entry) => {
const parameterName = entry[0];
if (map[parameterName]) {
map[parameterName].sensitivity = {
name: parameterName,
sensitive: entry[1] !== 'NON_SENSITIVE'
};
} else {
map[parameterName] = {
name: parameterName,
sensitivity: {
name: parameterName,
sensitive: entry[1] !== 'NON_SENSITIVE'
}
} as FetchedParameterMapping;
}
});
}
Object.entries(map).forEach((entry) => {
const paramName = entry[0];
const mapping: FetchedParameterMapping = entry[1];
mapping.name = paramName;
if (!mapping.sensitivity) {
// no known sensitivity, provide one
mapping.sensitivity = {
name: paramName,
sensitive: true
};
}
if (!mapping.status) {
mapping.status = {
status: 'UNCHANGED'
};
}
});
return map;
}
private parameterMappingArray(parameterGroupConfig: ParameterGroupConfiguration): FetchedParameterMapping[] {
return Object.values(this.getParameterMapping(parameterGroupConfig));
}
private getParameterSensitivitiesAsFormControls(parameterGroupConfig: ParameterGroupConfiguration): {
[key: string]: FormControl;
} {
const data: { [key: string]: FormControl } = {};
Object.entries(this.getParameterMapping(parameterGroupConfig)).forEach((entry) => {
const parameterName = entry[0];
const param: FetchedParameterMapping = entry[1];
if (parameterName && param?.sensitivity) {
data[parameterName] = new FormControl({
value: param.sensitivity.sensitive,
disabled: this.isReferenced(param)
});
}
});
return data;
}
sort(sort: Sort) {
this.setActiveSort(sort, this.selectedParameterGroup!);
const dataSource: MatTableDataSource<FetchedParameterMapping> = this.getParameterMappingDataSource(
this.selectedParameterGroup!
);
dataSource.data = this.sortEntities(dataSource.data, sort);
}
private sortEntities(data: FetchedParameterMapping[], sort: Sort): FetchedParameterMapping[] {
if (!data) {
return [];
}
return data.slice().sort((a, b) => {
const isAsc = sort.direction === 'asc';
const retVal = this.nifiCommon.compareString(a.name, b.name);
return retVal * (isAsc ? 1 : -1);
});
}
selectAllChanged(event: MatCheckboxChange) {
const checked: boolean = event.checked;
const currentParamGroup = this.selectedParameterGroup!;
const dataSource = this.getParameterMappingDataSource(currentParamGroup);
dataSource.data.forEach((p) => {
if (p.sensitivity) {
const formControl = this.getFormControl(p.sensitivity, currentParamGroup);
if (formControl && !formControl.disabled) {
formControl.setValue(checked);
formControl.markAsDirty();
}
}
});
}
areAllSelected(parameterGroupConfig: ParameterGroupConfiguration): boolean {
const dataSource = this.getParameterMappingDataSource(parameterGroupConfig);
let allSensitive = true;
dataSource.data.forEach((p) => {
if (p.sensitivity) {
const formControl = this.getFormControl(p.sensitivity, parameterGroupConfig);
if (formControl) {
allSensitive = allSensitive && formControl.value;
}
}
});
return allSensitive;
}
areAnySelected(parameterGroupConfig: ParameterGroupConfiguration): boolean {
const dataSource = this.getParameterMappingDataSource(parameterGroupConfig);
let anySensitive = false;
let allSensitive = true;
dataSource.data.forEach((p) => {
if (p.sensitivity) {
const formControl = this.getFormControl(p.sensitivity, parameterGroupConfig);
if (formControl) {
anySensitive = anySensitive || formControl.value;
allSensitive = allSensitive && formControl.value;
}
}
});
return anySensitive && !allSensitive;
}
isReferenced(item: FetchedParameterMapping): boolean {
const parameterStatus = item.status;
if (!parameterStatus?.parameter) {
return false;
}
const hasReferencingComponents = parameterStatus?.parameter.parameter.referencingComponents?.length;
return !!hasReferencingComponents;
}
isAffected(item: FetchedParameterMapping): boolean {
const parameterStatus = item.status;
if (!parameterStatus) {
return false;
}
return parameterStatus.status !== 'UNCHANGED';
}
getAffectedTooltip(item: FetchedParameterMapping): string | null {
switch (item.status?.status) {
case 'NEW':
return 'Newly discovered parameter.';
case 'CHANGED':
return 'Value has changed.';
case 'REMOVED':
return 'Parameter has been removed from its source. Apply to remove from the synced parameter context.';
case 'MISSING_BUT_REFERENCED':
return 'Parameter has been removed from its source and is still being referenced in a component. To remove the parameter from the parameter context, first un-reference the parameter, then re-fetch and apply.';
}
return null;
}
createParameterContextToggled(event: MatCheckboxChange) {
const checked: boolean = event.checked;
const currentParamGroup = this.selectedParameterGroup!;
this.parameterGroupConfigurations = this.parameterGroupConfigurations.map((config) => {
if (config.groupName === currentParamGroup.groupName) {
config = {
...config,
synchronized: checked
};
if (checked) {
this.parameterContextsToCreate[config.groupName] = config.parameterContextName;
// preload the datasource into the map
this.getParameterMappingDataSource(currentParamGroup);
this.autoSelectParameter();
} else {
delete this.parameterContextsToCreate[config.groupName];
// set the selected parameter to nothing
this.removeParameterSelection();
}
}
return config;
});
}
selectParameter(item: FetchedParameterMapping) {
this.selectParameterChanged.next(item);
}
isParameterSelected(item: FetchedParameterMapping): boolean {
const parameterGroupName = this.selectedParameterGroup?.groupName;
if (!parameterGroupName) {
return false;
}
const selectedParameter = this.selectedParameters[parameterGroupName];
if (!selectedParameter) {
return false;
}
if (
selectedParameter.name === item.name &&
selectedParameter.status?.parameter?.parameter.parameterContext?.id ===
item.status?.parameter?.parameter.parameterContext?.id
) {
return true;
}
return false;
}
private removeParameterSelection() {
this.selectParameterChanged.next(null);
}
canSubmitForm(): boolean {
// user needs to have read/write permissions on the component
const referencingParameterContexts = this.parameterProvider.component.referencingParameterContexts;
if (referencingParameterContexts?.length > 0) {
// disable the submit if one of the referenced parameter contexts is not readable/writeable
const canReadWriteAllParamContexts = referencingParameterContexts.every(
(paramContextRef) => paramContextRef.permissions.canRead && paramContextRef.permissions.canWrite
);
if (!canReadWriteAllParamContexts) {
return false;
}
}
const affectedComponents = this.parameterProvider.component.affectedComponents;
if (affectedComponents?.length > 0) {
// disable the submit if one of the affected components is not readable/writeable
const canReadWriteAllAffectedComponents = affectedComponents.every(
(affected) => affected.permissions.canRead && affected.permissions.canWrite
);
if (!canReadWriteAllAffectedComponents) {
return false;
}
}
// check if a parameter is new, removed, missing but referenced, or has a changed value
const parameterStatus = this.parameterProvider.component.parameterStatus;
let anyParametersChangedInProvider = false;
if (parameterStatus && parameterStatus.length > 0) {
anyParametersChangedInProvider = parameterStatus.some((paramStatus) => {
return paramStatus.status !== 'UNCHANGED';
});
}
// if a fetched parameter is new, removed, missing but referenced, or has a changed value... consider the form dirty.
const isDirty = anyParametersChangedInProvider || this.fetchParametersForm.dirty;
return isDirty && !this.fetchParametersForm.invalid;
}
private getFormData(): ParameterProviderParameterApplicationEntity {
const groupConfigs: ParameterGroupConfiguration[] = this.parameterGroupConfigurations
.filter((initialGroup) => {
// filter out any non-synchronized groups that the user hasn't decided to create a parameter context for
const createParameterContext = this.fetchParametersForm.get(
`${initialGroup.groupName}.createParameterContext`
);
return initialGroup.synchronized || !!createParameterContext?.value;
})
.map((initialGroup) => {
const parameterSensitivities: { [key: string]: null | 'SENSITIVE' | 'NON_SENSITIVE' } = {};
const parameterContextName = this.fetchParametersForm.get(
`${initialGroup.groupName}.parameterContextName`
)?.value;
// convert to the backend model for sensitivities
Object.entries(initialGroup.parameterSensitivities).forEach(([key, value]) => {
const formParamSensitivity = this.fetchParametersForm.get(
`${initialGroup.groupName}.parameterSensitivities.${key}`
);
if (formParamSensitivity) {
parameterSensitivities[key] = formParamSensitivity.value ? 'SENSITIVE' : 'NON_SENSITIVE';
} else {
// if there is no value defined by the form, send the known value
parameterSensitivities[key] = value;
}
});
return {
...initialGroup,
parameterContextName,
parameterSensitivities
};
});
return {
id: this.parameterProvider.id,
revision: this.parameterProvider.revision,
disconnectedNodeAcknowledged: false,
parameterGroupConfigurations: groupConfigs
};
}
}

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.
-->
<div class="parameter-group-table h-full flex flex-col">
<div class="flex-1 relative">
<div class="listing-table overflow-y-auto border absolute inset-0">
<table
mat-table
[dataSource]="parameterGroupsDataSource"
matSort
matSortDisableClear
matSortActive="groupName"
matSortDirection="asc"
(matSortChange)="sort($event)">
<ng-container matColumnDef="groupName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Parameter Group Name</th>
<td mat-cell *matCellDef="let item" class="items-center">
<div>{{ item.groupName }}</div>
</td>
</ng-container>
<ng-container matColumnDef="indicators">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<div class="flex items-center gap-x-3">
<div
class="fa fa-star"
title="Synced to a parameter context."
*ngIf="isSyncedToParameterContext(item)"></div>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr
mat-row
*matRowDef="let row; let even = even; columns: displayedColumns"
(click)="select(row)"
[class.selected]="isSelected(row)"
[class.even]="even"></tr>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
/*!
* 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.
*/
.parameter-group-table {
.listing-table {
.mat-column-indicators {
width: 32px;
min-width: 32px;
}
}
}

View File

@ -0,0 +1,39 @@
/*
* 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 { ParameterGroupsTable } from './parameter-groups-table.component';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('ParameterGroupsTable', () => {
let component: ParameterGroupsTable;
let fixture: ComponentFixture<ParameterGroupsTable>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ParameterGroupsTable, NoopAnimationsModule]
});
fixture = TestBed.createComponent(ParameterGroupsTable);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,96 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ParameterGroupConfiguration } from '../../../../state/parameter-providers';
import { NiFiCommon } from '../../../../../../service/nifi-common.service';
@Component({
selector: 'parameter-groups-table',
standalone: true,
imports: [CommonModule, MatSortModule, MatTableModule],
templateUrl: './parameter-groups-table.component.html',
styleUrls: ['./parameter-groups-table.component.scss']
})
export class ParameterGroupsTable {
parameterGroupsDataSource: MatTableDataSource<ParameterGroupConfiguration> =
new MatTableDataSource<ParameterGroupConfiguration>();
selectedParameterGroup: ParameterGroupConfiguration | null = null;
displayedColumns: string[] = ['groupName', 'indicators'];
activeParameterGroupSort: Sort = {
active: 'groupName',
direction: 'asc'
};
constructor(private nifiCommon: NiFiCommon) {}
@Input() set parameterGroups(parameterGroups: ParameterGroupConfiguration[]) {
this.parameterGroupsDataSource.data = this.sortEntities(parameterGroups, this.activeParameterGroupSort);
if (this.parameterGroupsDataSource.data.length > 0) {
let selectedIndex = 0;
// try to re-select the currently selected group if it still exists
if (this.selectedParameterGroup) {
const idx = this.parameterGroupsDataSource.data.findIndex(
(g) => g.groupName === this.selectedParameterGroup?.groupName
);
if (idx >= 0) {
selectedIndex = idx;
}
}
this.select(this.parameterGroupsDataSource.data[selectedIndex]);
}
}
@Output() selected: EventEmitter<ParameterGroupConfiguration> = new EventEmitter<ParameterGroupConfiguration>();
sort(sort: Sort) {
this.activeParameterGroupSort = sort;
this.parameterGroupsDataSource.data = this.sortEntities(this.parameterGroupsDataSource.data, sort);
}
private sortEntities(data: ParameterGroupConfiguration[], sort: Sort) {
if (!data) {
return [];
}
return data.slice().sort((a, b) => {
const isAsc = sort.direction === 'asc';
const retVal = this.nifiCommon.compareString(a.groupName, b.groupName);
return retVal * (isAsc ? 1 : -1);
});
}
select(item: ParameterGroupConfiguration) {
this.selectedParameterGroup = item;
this.selected.next(item);
}
isSelected(item: ParameterGroupConfiguration): boolean {
if (this.selectedParameterGroup) {
return item.groupName === this.selectedParameterGroup.groupName;
}
return false;
}
isSyncedToParameterContext(item: ParameterGroupConfiguration): boolean {
return !!item.synchronized;
}
}

View File

@ -86,14 +86,17 @@
<td mat-cell *matCellDef="let item">
<div class="flex items-center gap-x-3">
<div
*ngIf="canConfigure(item)"
class="pointer fa fa-pencil"
(click)="configureClicked(item, $event)"
title="Edit"></div>
<div
*ngIf="canFetch(item)"
class="pointer fa fa-arrow-circle-down"
(click)="fetchClicked(item, $event)"
title="Fetch Parameters"></div>
<div
*ngIf="canDelete(item)"
class="pointer fa fa-trash"
(click)="deleteClicked(item, $event)"
title="Remove"></div>

View File

@ -94,6 +94,34 @@ export class ParameterProvidersTable {
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
}
canConfigure(entity: ParameterProviderEntity): boolean {
return this.canRead(entity) && this.canWrite(entity);
}
canDelete(entity: ParameterProviderEntity): boolean {
return (
this.canRead(entity) &&
this.canWrite(entity) &&
this.currentUser.controllerPermissions.canRead &&
this.currentUser.controllerPermissions.canWrite
);
}
canFetch(entity: ParameterProviderEntity): boolean {
let hasReadParameterContextsPermissions = true;
if (this.canRead(entity) && entity.component.referencingParameterContexts) {
hasReadParameterContextsPermissions = entity.component.referencingParameterContexts.every(
(context) => context.permissions.canRead
);
}
return (
this.canRead(entity) &&
this.canWrite(entity) &&
hasReadParameterContextsPermissions &&
!this.hasErrors(entity)
);
}
isSelected(parameterProvider: ParameterProviderEntity): boolean {
if (this.selectedParameterProviderId) {
return parameterProvider.id === this.selectedParameterProviderId;

View File

@ -37,6 +37,7 @@
[selectedParameterProviderId]="selectedParameterProviderId$ | async"
(deleteParameterProvider)="deleteParameterProvider($event)"
(configureParameterProvider)="openConfigureParameterProviderDialog($event)"
(fetchParameterProvider)="fetchParameterProviderParameters($event)"
(selectParameterProvider)="selectParameterProvider($event)"></parameter-providers-table>
</div>
<div class="flex justify-between">

View File

@ -24,7 +24,8 @@ import {
selectParameterProvider,
selectParameterProviderIdFromRoute,
selectParameterProvidersState,
selectSingleEditedParameterProvider
selectSingleEditedParameterProvider,
selectSingleFetchParameterProvider
} from '../../state/parameter-providers/parameter-providers.selectors';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
@ -55,12 +56,34 @@ export class ParameterProviders implements OnInit, OnDestroy {
),
takeUntilDestroyed()
)
.subscribe((entity) => {
if (entity) {
this.store.dispatch(
ParameterProviderActions.openConfigureParameterProviderDialog({
request: {
id: entity.id,
parameterProvider: entity
}
})
);
}
});
this.store
.select(selectSingleFetchParameterProvider)
.pipe(
isDefinedAndNotNull(),
switchMap((id: string) =>
this.store.select(selectParameterProvider(id)).pipe(isDefinedAndNotNull(), take(1))
),
takeUntilDestroyed()
)
.subscribe((entity) => {
this.store.dispatch(
ParameterProviderActions.openConfigureParameterProviderDialog({
ParameterProviderActions.fetchParameterProviderParametersAndOpenDialog({
request: {
id: entity.id,
parameterProvider: entity
revision: entity.revision
}
})
);
@ -116,4 +139,12 @@ export class ParameterProviders implements OnInit, OnDestroy {
})
);
}
fetchParameterProviderParameters(parameterProvider: ParameterProviderEntity) {
this.store.dispatch(
ParameterProviderActions.navigateToFetchParameterProvider({
id: parameterProvider.component.id
})
);
}
}

View File

@ -15,14 +15,16 @@
* limitations under the License.
*/
import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
import { AfterViewInit, Component, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounceTime } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export interface SummaryTableFilterColumn {
key: string;
label: string;
}
export interface SummaryTableFilterArgs {
filterTerm: string;
filterColumn: string;
@ -40,6 +42,7 @@ export class SummaryTableFilter implements AfterViewInit {
private _filteredCount = 0;
private _totalCount = 0;
private _initialFilterColumn = 'name';
private destroyRef: DestroyRef = inject(DestroyRef);
showFilterMatchedLabel = false;
@Input() filterableColumns: SummaryTableFilterColumn[] = [];
@ -50,6 +53,7 @@ export class SummaryTableFilter implements AfterViewInit {
@Input() set filterTerm(term: string) {
this.filterForm.get('filterTerm')?.value(term);
}
@Input() set filterColumn(column: string) {
this._initialFilterColumn = column;
if (this.filterableColumns?.length > 0) {
@ -97,7 +101,7 @@ export class SummaryTableFilter implements AfterViewInit {
ngAfterViewInit(): void {
this.filterForm
.get('filterTerm')
?.valueChanges.pipe(debounceTime(500))
?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
.subscribe((filterTerm: string) => {
const filterColumn = this.filterForm.get('filterColumn')?.value;
const filterStatus = this.filterForm.get('filterStatus')?.value;
@ -105,26 +109,35 @@ export class SummaryTableFilter implements AfterViewInit {
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
const filterStatus = this.filterForm.get('filterStatus')?.value;
const primaryOnly = this.filterForm.get('primaryOnly')?.value;
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
this.filterForm
.get('filterColumn')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
const filterStatus = this.filterForm.get('filterStatus')?.value;
const primaryOnly = this.filterForm.get('primaryOnly')?.value;
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
this.filterForm.get('filterStatus')?.valueChanges.subscribe((filterStatus: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
const filterColumn = this.filterForm.get('filterColumn')?.value;
const primaryOnly = this.filterForm.get('primaryOnly')?.value;
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
this.filterForm
.get('filterStatus')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((filterStatus: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
const filterColumn = this.filterForm.get('filterColumn')?.value;
const primaryOnly = this.filterForm.get('primaryOnly')?.value;
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
this.filterForm.get('primaryOnly')?.valueChanges.subscribe((primaryOnly: boolean) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
const filterColumn = this.filterForm.get('filterColumn')?.value;
const filterStatus = this.filterForm.get('filterStatus')?.value;
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
this.filterForm
.get('primaryOnly')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((primaryOnly: boolean) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
const filterColumn = this.filterForm.get('filterColumn')?.value;
const filterStatus = this.filterForm.get('filterStatus')?.value;
this.applyFilter(filterTerm, filterColumn, filterStatus, primaryOnly);
});
}
applyFilter(filterTerm: string, filterColumn: string, filterStatus: string, primaryOnly: boolean) {

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
import { AfterViewInit, Component, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSortModule, Sort } from '@angular/material/sort';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
@ -27,6 +27,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { NgIf } from '@angular/common';
import { MatInputModule } from '@angular/material/input';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
export interface TenantItem {
id: string;
@ -68,6 +69,7 @@ export class UserTable implements AfterViewInit {
userLookup: Map<string, UserEntity> = new Map<string, UserEntity>();
userGroupLookup: Map<string, UserGroupEntity> = new Map<string, UserGroupEntity>();
private destroyRef: DestroyRef = inject(DestroyRef);
@Input() set tenants(tenants: Tenants) {
this.userLookup.clear();
@ -141,16 +143,19 @@ export class UserTable implements AfterViewInit {
ngAfterViewInit(): void {
this.filterForm
.get('filterTerm')
?.valueChanges.pipe(debounceTime(500))
?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
.subscribe((filterTerm: string) => {
const filterColumn = this.filterForm.get('filterColumn')?.value;
this.applyFilter(filterTerm, filterColumn);
});
this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
this.applyFilter(filterTerm, filterColumn);
});
this.filterForm
.get('filterColumn')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
this.applyFilter(filterTerm, filterColumn);
});
}
applyFilter(filterTerm: string, filterColumn: string) {

View File

@ -0,0 +1,46 @@
/*
* 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 { JoinPipe } from './join.pipe';
describe('JoinPipe', () => {
const values: string[] = ['alpha', 'omega', 'beta', 'theta', 'phi'];
const numbers: number[] = [1, 2, 3];
it('create an instance', () => {
const pipe = new JoinPipe();
expect(pipe).toBeTruthy();
});
it('should join by comma-space by default', () => {
const pipe = new JoinPipe();
const joined = pipe.transform(values);
expect(joined).toEqual('alpha, omega, beta, theta, phi');
});
it('should join by a specified separator', () => {
const pipe = new JoinPipe();
const joined = pipe.transform(values, '-');
expect(joined).toEqual('alpha-omega-beta-theta-phi');
});
it('should join numbers', () => {
const pipe = new JoinPipe();
const joined = pipe.transform(numbers, ' | ');
expect(joined).toEqual('1 | 2 | 3');
});
});

View File

@ -0,0 +1,27 @@
/*
* 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 { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'join'
})
export class JoinPipe implements PipeTransform {
transform(array: any[], separator = ', '): string {
return array.join(separator);
}
}

View File

@ -0,0 +1,28 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { JoinPipe } from './join.pipe';
import { SortPipe } from './sort.pipe';
@NgModule({
declarations: [SortPipe, JoinPipe],
exports: [SortPipe, JoinPipe],
imports: [CommonModule]
})
export class PipesModule {}

View File

@ -0,0 +1,41 @@
/*
* 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 { SortPipe } from './sort.pipe';
describe('SortPipe', () => {
const values: string[] = ['alpha', 'omega', 'beta', 'theta', 'phi'];
it('create an instance', () => {
const pipe = new SortPipe();
expect(pipe).toBeTruthy();
});
it('sorts ascending by default', () => {
const pipe = new SortPipe();
const sorted: string[] = pipe.transform(values);
expect(sorted).toEqual(['alpha', 'beta', 'omega', 'phi', 'theta']);
});
it('sorts descending', () => {
const pipe = new SortPipe();
const sorted: string[] = pipe.transform(values, 'desc');
expect(sorted).toEqual(['theta', 'phi', 'omega', 'beta', 'alpha']);
});
});

View File

@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Pipe, PipeTransform } from '@angular/core';
type Direction = 'asc' | 'desc';
@Pipe({
name: 'sort'
})
export class SortPipe implements PipeTransform {
transform(array: string[], direction: Direction = 'asc'): string[] {
if (!array || array.length === 0) {
return [];
}
return array.slice().sort((a, b) => {
const isAsc = direction === 'asc';
const retVal = this.compareString(a, b);
return retVal * (isAsc ? 1 : -1);
});
}
private compareString(a: string, b: string): number {
if (a === b) {
return 0;
}
return a < b ? -1 : 1;
}
}

View File

@ -43,6 +43,8 @@ export class AuthInterceptor implements HttpInterceptor {
if (errorResponse instanceof HttpErrorResponse) {
if (errorResponse.status === 401) {
if (this.authStorage.hasToken()) {
this.routedToFullScreenError = true;
this.authStorage.removeToken();
let message: string = errorResponse.error;
@ -52,8 +54,6 @@ export class AuthInterceptor implements HttpInterceptor {
message += '. Please navigate home to log in again.';
}
this.routedToFullScreenError = true;
this.store.dispatch(
fullScreenError({
errorDetail: {

View File

@ -21,13 +21,15 @@ import * as ErrorActions from './error.actions';
import { map, tap } from 'rxjs';
import { Router } from '@angular/router';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatDialog } from '@angular/material/dialog';
@Injectable()
export class ErrorEffects {
constructor(
private actions$: Actions,
private router: Router,
private snackBar: MatSnackBar
private snackBar: MatSnackBar,
private dialog: MatDialog
) {}
fullScreenError$ = createEffect(
@ -35,6 +37,7 @@ export class ErrorEffects {
this.actions$.pipe(
ofType(ErrorActions.fullScreenError),
tap(() => {
this.dialog.openDialogs.forEach((dialog) => dialog.close('ROUTED'));
this.router.navigate(['/error'], { replaceUrl: true });
})
),

View File

@ -21,7 +21,7 @@ export function isDefinedAndNotNull<T>() {
return (source$: Observable<null | undefined | T>) =>
source$.pipe(
filter((input: null | undefined | T): input is T => {
return input !== null && typeof input !== undefined;
return input !== null && typeof input !== 'undefined';
})
);
}
@ -594,3 +594,15 @@ export interface CreateControllerServiceRequest {
export interface ControllerServiceCreator {
createControllerService(createControllerService: CreateControllerServiceRequest): Observable<any>;
}
export interface ParameterProviderConfiguration {
parameterGroupName: string;
parameterProviderId: string;
parameterProviderName: string;
synchronized: boolean;
}
export interface ParameterProviderConfigurationEntity {
id: string;
component: ParameterProviderConfiguration;
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { AfterViewInit, Component, Input } from '@angular/core';
import { AfterViewInit, Component, DestroyRef, inject, Input } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
@ -75,6 +75,7 @@ export class ComponentStateDialog implements AfterViewInit {
totalEntries = 0;
filteredEntries = 0;
partialResults = false;
private destroyRef: DestroyRef = inject(DestroyRef);
constructor(
private store: Store<ComponentStateState>,
@ -116,7 +117,7 @@ export class ComponentStateDialog implements AfterViewInit {
ngAfterViewInit(): void {
this.filterForm
.get('filterTerm')
?.valueChanges.pipe(debounceTime(500))
?.valueChanges.pipe(debounceTime(500), takeUntilDestroyed(this.destroyRef))
.subscribe((filterTerm: string) => {
this.applyFilter(filterTerm);
});

View File

@ -136,7 +136,7 @@
<div class="fa fa-spin fa-circle-o-notch"></div>
</ng-template>
<ng-template #stepComplete>
<div class="fa fa-check text-green-500"></div>
<div class="fa fa-check complete"></div>
</ng-template>
<ng-template #stepError>
<div class="fa fa-times text-red-400"></div>

View File

@ -165,7 +165,7 @@
<div class="fa fa-spin fa-circle-o-notch"></div>
</ng-template>
<ng-template #stepComplete>
<div class="fa fa-check text-green-500"></div>
<div class="fa fa-check complete"></div>
</ng-template>
<ng-template #stepError>
<div class="fa fa-times text-red-400"></div>

View File

@ -22,17 +22,17 @@ import { MatButtonModule } from '@angular/material/button';
import { NgClass, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { RouterLink } from '@angular/router';
import { MatDialogModule } from '@angular/material/dialog';
import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive';
import { NifiTooltipDirective } from '../tooltips/nifi-tooltip.directive';
import {
AffectedComponent,
AffectedComponentEntity,
BulletinsTipInput,
ProcessGroupName,
ValidationErrorsTipInput
} from '../../../../../state/shared';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
} from '../../../state/shared';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { ValidationErrorsTip } from '../tooltips/validation-errors-tip/validation-errors-tip.component';
import { BulletinsTip } from '../tooltips/bulletins-tip/bulletins-tip.component';
@Component({
selector: 'parameter-references',

View File

@ -22,6 +22,7 @@ import * as d3 from 'd3';
import { NiFiCommon } from '../../../../service/nifi-common.service';
import { Instance, NIFI_NODE_CONFIG, Stats, VisibleInstances } from '../index';
import { debounceTime, Subject } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'status-history-chart',
@ -82,11 +83,11 @@ export class StatusHistoryChart {
constructor(private nifiCommon: NiFiCommon) {
// don't need constantly fire the stats changing as a result of brush drag/move
this.nodeStats$.pipe(debounceTime(20)).subscribe((stats: Stats) => {
this.nodeStats$.pipe(debounceTime(20), takeUntilDestroyed()).subscribe((stats: Stats) => {
this.nodeStats.next(stats);
});
this.clusterStats$.pipe(debounceTime(20)).subscribe((stats: Stats) => {
this.clusterStats$.pipe(debounceTime(20), takeUntilDestroyed()).subscribe((stats: Stats) => {
this.clusterStats.next(stats);
});
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
import { AfterViewInit, Component, Inject, OnInit } from '@angular/core';
import { AfterViewInit, Component, DestroyRef, inject, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { StatusHistoryService } from '../../../service/status-history.service';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
@ -50,6 +50,7 @@ import { MatCheckboxChange, MatCheckboxModule } from '@angular/material/checkbox
import { Resizable } from '../resizable/resizable.component';
import { Instance, NIFI_NODE_CONFIG, Stats } from './index';
import { StatusHistoryChart } from './status-history-chart/status-history-chart.component';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'status-history',
@ -100,6 +101,7 @@ export class StatusHistory implements OnInit, AfterViewInit {
instances: Instance[] = [];
instanceVisibility: any = {};
selectedDescriptor: FieldDescriptor | null = null;
private destroyRef: DestroyRef = inject(DestroyRef);
constructor(
private statusHistoryService: StatusHistoryService,
@ -115,60 +117,65 @@ export class StatusHistory implements OnInit, AfterViewInit {
}
ngOnInit(): void {
this.statusHistory$.pipe(filter((entity) => !!entity)).subscribe((entity: StatusHistoryEntity) => {
if (entity) {
this.instances = [];
if (entity.statusHistory?.aggregateSnapshots?.length > 1) {
this.instances.push({
id: NIFI_NODE_CONFIG.nifiInstanceId,
label: NIFI_NODE_CONFIG.nifiInstanceLabel,
snapshots: entity.statusHistory.aggregateSnapshots
});
// if this is the first time this instance is being rendered, make it visible
if (this.instanceVisibility[NIFI_NODE_CONFIG.nifiInstanceId] === undefined) {
this.instanceVisibility[NIFI_NODE_CONFIG.nifiInstanceId] = true;
}
}
// get the status for each node in the cluster if applicable
if (entity.statusHistory?.nodeSnapshots && entity.statusHistory?.nodeSnapshots.length > 1) {
entity.statusHistory.nodeSnapshots.forEach((nodeSnapshot: NodeSnapshot) => {
this.statusHistory$
.pipe(
filter((entity) => !!entity),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((entity: StatusHistoryEntity) => {
if (entity) {
this.instances = [];
if (entity.statusHistory?.aggregateSnapshots?.length > 1) {
this.instances.push({
id: nodeSnapshot.nodeId,
label: `${nodeSnapshot.address}:${nodeSnapshot.apiPort}`,
snapshots: nodeSnapshot.statusSnapshots
id: NIFI_NODE_CONFIG.nifiInstanceId,
label: NIFI_NODE_CONFIG.nifiInstanceLabel,
snapshots: entity.statusHistory.aggregateSnapshots
});
// if this is the first time this instance is being rendered, make it visible
if (this.instanceVisibility[nodeSnapshot.nodeId] === undefined) {
this.instanceVisibility[nodeSnapshot.nodeId] = true;
if (this.instanceVisibility[NIFI_NODE_CONFIG.nifiInstanceId] === undefined) {
this.instanceVisibility[NIFI_NODE_CONFIG.nifiInstanceId] = true;
}
}
// get the status for each node in the cluster if applicable
if (entity.statusHistory?.nodeSnapshots && entity.statusHistory?.nodeSnapshots.length > 1) {
entity.statusHistory.nodeSnapshots.forEach((nodeSnapshot: NodeSnapshot) => {
this.instances.push({
id: nodeSnapshot.nodeId,
label: `${nodeSnapshot.address}:${nodeSnapshot.apiPort}`,
snapshots: nodeSnapshot.statusSnapshots
});
// if this is the first time this instance is being rendered, make it visible
if (this.instanceVisibility[nodeSnapshot.nodeId] === undefined) {
this.instanceVisibility[nodeSnapshot.nodeId] = true;
}
});
}
// identify all nodes and sort
this.nodes = this.instances
.filter((status) => {
return status.id !== NIFI_NODE_CONFIG.nifiInstanceId;
})
.sort((a: any, b: any) => {
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
});
// determine the min/max date
const minDate: any = d3.min(this.instances, (d) => {
return d3.min(d.snapshots, (s) => {
return s.timestamp;
});
});
const maxDate: any = d3.max(this.instances, (d) => {
return d3.max(d.snapshots, (s) => {
return s.timestamp;
});
});
this.minDate = this.nifiCommon.formatDateTime(new Date(minDate));
this.maxDate = this.nifiCommon.formatDateTime(new Date(maxDate));
}
// identify all nodes and sort
this.nodes = this.instances
.filter((status) => {
return status.id !== NIFI_NODE_CONFIG.nifiInstanceId;
})
.sort((a: any, b: any) => {
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
});
// determine the min/max date
const minDate: any = d3.min(this.instances, (d) => {
return d3.min(d.snapshots, (s) => {
return s.timestamp;
});
});
const maxDate: any = d3.max(this.instances, (d) => {
return d3.max(d.snapshots, (s) => {
return s.timestamp;
});
});
this.minDate = this.nifiCommon.formatDateTime(new Date(minDate));
this.maxDate = this.nifiCommon.formatDateTime(new Date(maxDate));
}
});
});
this.fieldDescriptors$
.pipe(
filter((descriptors) => !!descriptors),
@ -185,11 +192,14 @@ export class StatusHistory implements OnInit, AfterViewInit {
ngAfterViewInit(): void {
// when the selected descriptor changes, update the chart
this.statusHistoryForm.get('fieldDescriptor')?.valueChanges.subscribe((descriptor: FieldDescriptor) => {
if (this.instances.length > 0) {
this.selectedDescriptor = descriptor;
}
});
this.statusHistoryForm
.get('fieldDescriptor')
?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((descriptor: FieldDescriptor) => {
if (this.instances.length > 0) {
this.selectedDescriptor = descriptor;
}
});
}
isInitialLoading(state: StatusHistoryState) {

View File

@ -388,6 +388,13 @@ $appFontPath: '~roboto-fontface/fonts';
min-width: 760px;
}
.xl-dialog {
max-height: 72%;
max-width: 85%;
min-height: 560px;
min-width: 1024px;
}
.edit-parameter-context-dialog {
max-height: 72%;
max-width: 55%;