mirror of https://github.com/apache/nifi.git
NIFI-12486: Registry Clients (#8142)
* NIFI-12486: - Registry Clients. - General authorization guard. - Additional authorization checks in the existing Settings tabs. * NIFI-12486: - Adding authorization guard to /counters. * NIFI-12486: - Enabling some debug build out to attempt to track down a sporadic build failure. * NIFI-12486: - Addressing review feedback. * NIFI-12486: - Fixing unit test and running prettier. This closes #8142
This commit is contained in:
parent
76613a0ed4
commit
b0f30d6860
|
@ -16,7 +16,12 @@
|
|||
*/
|
||||
package org.apache.nifi.web.api.entity;
|
||||
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import org.apache.nifi.web.api.dto.util.TimeAdapter;
|
||||
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
import java.util.Date;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
|
@ -25,6 +30,7 @@ import java.util.Set;
|
|||
@XmlRootElement(name = "registryClientsEntity")
|
||||
public class FlowRegistryClientsEntity extends Entity {
|
||||
|
||||
private Date currentTime;
|
||||
private Set<FlowRegistryClientEntity> registries;
|
||||
|
||||
/**
|
||||
|
@ -38,4 +44,19 @@ public class FlowRegistryClientsEntity extends Entity {
|
|||
this.registries = registries;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return current time on the server
|
||||
*/
|
||||
@XmlJavaTypeAdapter(TimeAdapter.class)
|
||||
@ApiModelProperty(
|
||||
value = "The current time on the system.",
|
||||
dataType = "string"
|
||||
)
|
||||
public Date getCurrentTime() {
|
||||
return currentTime;
|
||||
}
|
||||
|
||||
public void setCurrentTime(Date currentTime) {
|
||||
this.currentTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1483,6 +1483,7 @@ public class ControllerResource extends ApplicationResource {
|
|||
|
||||
final Set<FlowRegistryClientEntity> flowRegistryClients = serviceFacade.getRegistryClients();
|
||||
final FlowRegistryClientsEntity flowRegistryClientEntities = new FlowRegistryClientsEntity();
|
||||
flowRegistryClientEntities.setCurrentTime(new Date());
|
||||
flowRegistryClientEntities.setRegistries(flowRegistryClients);
|
||||
|
||||
return generateOkResponse(populateRemainingRegistryClientEntityContent(flowRegistryClientEntities)).build();
|
||||
|
|
|
@ -1838,6 +1838,7 @@ public class FlowResource extends ApplicationResource {
|
|||
|
||||
final Set<FlowRegistryClientEntity> registryClients = serviceFacade.getRegistryClientsForUser();
|
||||
final FlowRegistryClientsEntity registryClientEntities = new FlowRegistryClientsEntity();
|
||||
registryClientEntities.setCurrentTime(new Date());
|
||||
registryClientEntities.setRegistries(registryClients);
|
||||
|
||||
return generateOkResponse(populateRemainingRegistryClientEntityContent(registryClientEntities)).build();
|
||||
|
|
|
@ -37,7 +37,9 @@
|
|||
"maximumError": "3mb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
"outputHashing": "all",
|
||||
"buildOptimizer": false,
|
||||
"optimization": true
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"build": "ng build --verbose",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test --karma-config=karma.conf.js --watch=false",
|
||||
"prettier": "prettier --config .prettierrc . --check",
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { authGuard } from './service/guard/auth.guard';
|
||||
import { authenticationGuard } from './service/guard/authentication.guard';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -26,17 +26,17 @@ const routes: Routes = [
|
|||
},
|
||||
{
|
||||
path: 'settings',
|
||||
canMatch: [authGuard],
|
||||
canMatch: [authenticationGuard],
|
||||
loadChildren: () => import('./pages/settings/feature/settings.module').then((m) => m.SettingsModule)
|
||||
},
|
||||
{
|
||||
path: 'provenance',
|
||||
canMatch: [authGuard],
|
||||
canMatch: [authenticationGuard],
|
||||
loadChildren: () => import('./pages/provenance/feature/provenance.module').then((m) => m.ProvenanceModule)
|
||||
},
|
||||
{
|
||||
path: 'parameter-contexts',
|
||||
canMatch: [authGuard],
|
||||
canMatch: [authenticationGuard],
|
||||
loadChildren: () =>
|
||||
import('./pages/parameter-contexts/feature/parameter-contexts.module').then(
|
||||
(m) => m.ParameterContextsModule
|
||||
|
@ -44,12 +44,12 @@ const routes: Routes = [
|
|||
},
|
||||
{
|
||||
path: 'counters',
|
||||
canMatch: [authGuard],
|
||||
canMatch: [authenticationGuard],
|
||||
loadChildren: () => import('./pages/counters/feature/counters.module').then((m) => m.CountersModule)
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
canMatch: [authGuard],
|
||||
canMatch: [authenticationGuard],
|
||||
loadChildren: () =>
|
||||
import('./pages/flow-designer/feature/flow-designer.module').then((m) => m.FlowDesignerModule)
|
||||
}
|
||||
|
|
|
@ -18,11 +18,14 @@
|
|||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Counters } from './counters.component';
|
||||
import { authorizationGuard } from '../../../service/guard/authorization.guard';
|
||||
import { User } from '../../../state/user';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Counters
|
||||
component: Counters,
|
||||
canMatch: [authorizationGuard((user: User) => user.countersPermissions.canRead)]
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
controllerServicesApiError,
|
||||
createControllerService,
|
||||
createControllerServiceSuccess,
|
||||
deleteControllerService,
|
||||
deleteControllerServiceSuccess,
|
||||
inlineCreateControllerServiceSuccess,
|
||||
loadControllerServices,
|
||||
|
@ -72,7 +73,7 @@ export const controllerServicesReducer = createReducer(
|
|||
error,
|
||||
status: 'error' as const
|
||||
})),
|
||||
on(createControllerService, (state, { request }) => ({
|
||||
on(createControllerService, configureControllerService, deleteControllerService, (state, { request }) => ({
|
||||
...state,
|
||||
saving: true
|
||||
})),
|
||||
|
@ -87,10 +88,6 @@ export const controllerServicesReducer = createReducer(
|
|||
draftState.controllerServices.push(response.controllerService);
|
||||
});
|
||||
}),
|
||||
on(configureControllerService, (state, { request }) => ({
|
||||
...state,
|
||||
saving: true
|
||||
})),
|
||||
on(configureControllerServiceSuccess, (state, { response }) => {
|
||||
return produce(state, (draftState) => {
|
||||
const componentIndex: number = draftState.controllerServices.findIndex((f: any) => response.id === f.id);
|
||||
|
@ -108,6 +105,7 @@ export const controllerServicesReducer = createReducer(
|
|||
if (componentIndex > -1) {
|
||||
draftState.controllerServices.splice(componentIndex, 1);
|
||||
}
|
||||
draftState.saving = false;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
@ -90,7 +90,11 @@
|
|||
Data Provenance
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item class="global-menu-item" [routerLink]="['/settings']">
|
||||
<button
|
||||
mat-menu-item
|
||||
class="global-menu-item"
|
||||
[routerLink]="['/settings']"
|
||||
[disabled]="!user.controllerPermissions.canRead">
|
||||
<i class="fa fa-fw fa-wrench mr-2"></i>
|
||||
Controller Settings
|
||||
</button>
|
||||
|
|
|
@ -116,7 +116,10 @@ export class ProvenanceEventTable implements AfterViewInit {
|
|||
totalCount: number = 0;
|
||||
filteredCount: number = 0;
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {
|
||||
this.filterForm = this.formBuilder.group({ filterTerm: '', filterColumn: this.filterColumnOptions[0] });
|
||||
}
|
||||
|
||||
|
@ -145,35 +148,34 @@ export class ProvenanceEventTable implements AfterViewInit {
|
|||
return data.sort((a, b) => {
|
||||
const isAsc = sort.direction === 'asc';
|
||||
|
||||
let retVal: number = 0;
|
||||
switch (sort.active) {
|
||||
case 'eventTime':
|
||||
// event ideas are increasing, so we can use this simple number for sorting purposes
|
||||
// since we don't surface the timestamp as millis
|
||||
return (a.eventId - b.eventId) * (isAsc ? 1 : -1);
|
||||
retVal = this.nifiCommon.compareNumber(a.eventId, b.eventId);
|
||||
break;
|
||||
case 'eventType':
|
||||
return this.compare(a.eventType, b.eventType, isAsc);
|
||||
retVal = this.nifiCommon.compareString(a.eventType, b.eventType);
|
||||
break;
|
||||
case 'flowFileUuid':
|
||||
return this.compare(a.flowFileUuid, b.flowFileUuid, isAsc);
|
||||
retVal = this.nifiCommon.compareString(a.flowFileUuid, b.flowFileUuid);
|
||||
break;
|
||||
case 'fileSize':
|
||||
return (a.fileSizeBytes - b.fileSizeBytes) * (isAsc ? 1 : -1);
|
||||
retVal = this.nifiCommon.compareNumber(a.fileSizeBytes, b.fileSizeBytes);
|
||||
break;
|
||||
case 'componentName':
|
||||
return this.compare(a.componentName, b.componentName, isAsc);
|
||||
retVal = this.nifiCommon.compareString(a.componentName, b.componentName);
|
||||
break;
|
||||
case 'componentType':
|
||||
return this.compare(a.componentType, b.componentType, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
retVal = this.nifiCommon.compareString(a.componentType, b.componentType);
|
||||
break;
|
||||
}
|
||||
|
||||
return retVal * (isAsc ? 1 : -1);
|
||||
});
|
||||
}
|
||||
|
||||
private compare(a: string, b: string, isAsc: boolean): number {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
}
|
||||
|
||||
applyFilter(filterTerm: string, filterColumn: string) {
|
||||
this.dataSource.filter = `${filterTerm}|${filterColumn}`;
|
||||
this.filteredCount = this.dataSource.filteredData.length;
|
||||
|
|
|
@ -24,11 +24,14 @@ import { ReportingTasks } from '../ui/reporting-tasks/reporting-tasks.component'
|
|||
import { FlowAnalysisRules } from '../ui/flow-analysis-rules/flow-analysis-rules.component';
|
||||
import { RegistryClients } from '../ui/registry-clients/registry-clients.component';
|
||||
import { ParameterProviders } from '../ui/parameter-providers/parameter-providers.component';
|
||||
import { authorizationGuard } from '../../../service/guard/authorization.guard';
|
||||
import { User } from '../../../state/user';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Settings,
|
||||
canMatch: [authorizationGuard((user: User) => user.controllerPermissions.canRead)],
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'general' },
|
||||
{ path: 'general', component: General },
|
||||
|
@ -59,7 +62,22 @@ const routes: Routes = [
|
|||
]
|
||||
},
|
||||
{ path: 'flow-analysis-rules', component: FlowAnalysisRules },
|
||||
{ path: 'registry-clients', component: RegistryClients },
|
||||
{
|
||||
path: 'registry-clients',
|
||||
component: RegistryClients,
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: RegistryClients,
|
||||
children: [
|
||||
{
|
||||
path: 'edit',
|
||||
component: RegistryClients
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ path: 'parameter-providers', component: ParameterProviders }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import { RegistryClientsModule } from '../ui/registry-clients/registry-clients.m
|
|||
import { ReportingTasksModule } from '../ui/reporting-tasks/reporting-tasks.module';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { ReportingTasksEffects } from '../state/reporting-tasks/reporting-tasks.effects';
|
||||
import { RegistryClientsEffects } from '../state/registry-clients/registry-clients.effects';
|
||||
|
||||
@NgModule({
|
||||
declarations: [Settings],
|
||||
|
@ -46,7 +47,12 @@ import { ReportingTasksEffects } from '../state/reporting-tasks/reporting-tasks.
|
|||
ReportingTasksModule,
|
||||
SettingsRoutingModule,
|
||||
StoreModule.forFeature(settingsFeatureKey, reducers),
|
||||
EffectsModule.forFeature(GeneralEffects, ManagementControllerServicesEffects, ReportingTasksEffects),
|
||||
EffectsModule.forFeature(
|
||||
GeneralEffects,
|
||||
ManagementControllerServicesEffects,
|
||||
ReportingTasksEffects,
|
||||
RegistryClientsEffects
|
||||
),
|
||||
MatTabsModule
|
||||
]
|
||||
})
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Client } from '../../../service/client.service';
|
||||
import { NiFiCommon } from '../../../service/nifi-common.service';
|
||||
import {
|
||||
CreateRegistryClientRequest,
|
||||
DeleteRegistryClientRequest,
|
||||
EditRegistryClientRequest,
|
||||
RegistryClientEntity
|
||||
} from '../state/registry-clients';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class RegistryClientService {
|
||||
private static readonly API: string = '../nifi-api';
|
||||
|
||||
/**
|
||||
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
|
||||
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
|
||||
*
|
||||
* https://stackoverflow.com/a/59586462
|
||||
*
|
||||
* @param url
|
||||
* @private
|
||||
*/
|
||||
private stripProtocol(url: string): string {
|
||||
return this.nifiCommon.substringAfterFirst(url, ':');
|
||||
}
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private client: Client,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {}
|
||||
|
||||
getRegistryClients(): Observable<any> {
|
||||
return this.httpClient.get(`${RegistryClientService.API}/controller/registry-clients`);
|
||||
}
|
||||
|
||||
createRegistryClient(createReportingTask: CreateRegistryClientRequest): Observable<any> {
|
||||
return this.httpClient.post(`${RegistryClientService.API}/controller/registry-clients`, createReportingTask);
|
||||
}
|
||||
|
||||
getPropertyDescriptor(id: string, propertyName: string, sensitive: boolean): Observable<any> {
|
||||
const params: any = {
|
||||
propertyName,
|
||||
sensitive
|
||||
};
|
||||
return this.httpClient.get(`${RegistryClientService.API}/controller/registry-clients/${id}/descriptors`, {
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
updateRegistryClient(request: EditRegistryClientRequest): Observable<any> {
|
||||
return this.httpClient.put(this.stripProtocol(request.uri), request.payload);
|
||||
}
|
||||
|
||||
deleteRegistryClient(deleteRegistryClient: DeleteRegistryClientRequest): Observable<any> {
|
||||
const entity: RegistryClientEntity = deleteRegistryClient.registryClient;
|
||||
const revision: any = this.client.getRevision(entity);
|
||||
return this.httpClient.delete(this.stripProtocol(entity.uri), { params: revision });
|
||||
}
|
||||
}
|
|
@ -15,10 +15,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Canvas Positioning/Transforms
|
||||
*/
|
||||
|
||||
import { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
|
||||
import { GeneralState, generalFeatureKey } from './general';
|
||||
import { generalReducer } from './general/general.reducer';
|
||||
|
@ -29,6 +25,8 @@ import {
|
|||
import { managementControllerServicesReducer } from './management-controller-services/management-controller-services.reducer';
|
||||
import { reportingTasksFeatureKey, ReportingTasksState } from './reporting-tasks';
|
||||
import { reportingTasksReducer } from './reporting-tasks/reporting-tasks.reducer';
|
||||
import { registryClientsFeatureKey, RegistryClientsState } from './registry-clients';
|
||||
import { registryClientsReducer } from './registry-clients/registry-clients.reducer';
|
||||
|
||||
export const settingsFeatureKey = 'settings';
|
||||
|
||||
|
@ -36,13 +34,15 @@ export interface SettingsState {
|
|||
[generalFeatureKey]: GeneralState;
|
||||
[managementControllerServicesFeatureKey]: ManagementControllerServicesState;
|
||||
[reportingTasksFeatureKey]: ReportingTasksState;
|
||||
[registryClientsFeatureKey]: RegistryClientsState;
|
||||
}
|
||||
|
||||
export function reducers(state: SettingsState | undefined, action: Action) {
|
||||
return combineReducers({
|
||||
[generalFeatureKey]: generalReducer,
|
||||
[managementControllerServicesFeatureKey]: managementControllerServicesReducer,
|
||||
[reportingTasksFeatureKey]: reportingTasksReducer
|
||||
[reportingTasksFeatureKey]: reportingTasksReducer,
|
||||
[registryClientsFeatureKey]: registryClientsReducer
|
||||
})(state, action);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import * as ManagementControllerServicesActions from './management-controller-services.actions';
|
||||
import { catchError, from, map, NEVER, Observable, of, switchMap, take, tap, withLatestFrom } from 'rxjs';
|
||||
import { catchError, from, map, NEVER, Observable, of, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
@ -139,15 +139,23 @@ export class ManagementControllerServicesEffects {
|
|||
)
|
||||
);
|
||||
|
||||
createControllerServiceSuccess$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ManagementControllerServicesActions.createControllerServiceSuccess),
|
||||
tap(() => {
|
||||
this.dialog.closeAll();
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
createControllerServiceSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ManagementControllerServicesActions.createControllerServiceSuccess),
|
||||
map((action) => action.response),
|
||||
tap(() => {
|
||||
this.dialog.closeAll();
|
||||
}),
|
||||
switchMap((response) =>
|
||||
of(
|
||||
ManagementControllerServicesActions.selectControllerService({
|
||||
request: {
|
||||
id: response.controllerService.id
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
navigateToEditService$ = createEffect(
|
||||
|
@ -294,7 +302,7 @@ export class ManagementControllerServicesEffects {
|
|||
};
|
||||
|
||||
editDialogReference.componentInstance.editControllerService
|
||||
.pipe(take(1))
|
||||
.pipe(takeUntil(editDialogReference.afterClosed()))
|
||||
.subscribe((payload: any) => {
|
||||
this.store.dispatch(
|
||||
ManagementControllerServicesActions.configureControllerService({
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
configureControllerServiceSuccess,
|
||||
createControllerService,
|
||||
createControllerServiceSuccess,
|
||||
deleteControllerService,
|
||||
deleteControllerServiceSuccess,
|
||||
inlineCreateControllerServiceSuccess,
|
||||
loadManagementControllerServices,
|
||||
|
@ -57,7 +58,7 @@ export const managementControllerServicesReducer = createReducer(
|
|||
error,
|
||||
status: 'error' as const
|
||||
})),
|
||||
on(createControllerService, (state, { request }) => ({
|
||||
on(createControllerService, configureControllerService, deleteControllerService, (state, { request }) => ({
|
||||
...state,
|
||||
saving: true
|
||||
})),
|
||||
|
@ -72,10 +73,6 @@ export const managementControllerServicesReducer = createReducer(
|
|||
draftState.controllerServices.push(response.controllerService);
|
||||
});
|
||||
}),
|
||||
on(configureControllerService, (state, { request }) => ({
|
||||
...state,
|
||||
saving: true
|
||||
})),
|
||||
on(configureControllerServiceSuccess, (state, { response }) => {
|
||||
return produce(state, (draftState) => {
|
||||
const componentIndex: number = draftState.controllerServices.findIndex((f: any) => response.id === f.id);
|
||||
|
@ -93,6 +90,7 @@ export const managementControllerServicesReducer = createReducer(
|
|||
if (componentIndex > -1) {
|
||||
draftState.controllerServices.splice(componentIndex, 1);
|
||||
}
|
||||
draftState.saving = false;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 { BulletinEntity, DocumentedType, Permissions, Revision } from '../../../../state/shared';
|
||||
|
||||
export const registryClientsFeatureKey = 'registryClients';
|
||||
|
||||
export interface CreateRegistryClientDialogRequest {
|
||||
registryClientTypes: DocumentedType[];
|
||||
}
|
||||
|
||||
export interface LoadRegistryClientsResponse {
|
||||
registryClients: RegistryClientEntity[];
|
||||
loadedTimestamp: string;
|
||||
}
|
||||
|
||||
export interface CreateRegistryClientRequest {
|
||||
revision: Revision;
|
||||
component: {
|
||||
name: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateRegistryClientSuccess {
|
||||
registryClient: RegistryClientEntity;
|
||||
}
|
||||
|
||||
export interface EditRegistryClientDialogRequest {
|
||||
registryClient: RegistryClientEntity;
|
||||
}
|
||||
|
||||
export interface EditRegistryClientRequest {
|
||||
id: string;
|
||||
uri: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
export interface EditRegistryClientRequestSuccess {
|
||||
id: string;
|
||||
registryClient: RegistryClientEntity;
|
||||
}
|
||||
|
||||
export interface DeleteRegistryClientRequest {
|
||||
registryClient: RegistryClientEntity;
|
||||
}
|
||||
|
||||
export interface DeleteRegistryClientSuccess {
|
||||
registryClient: RegistryClientEntity;
|
||||
}
|
||||
|
||||
export interface SelectRegistryClientRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface RegistryClientEntity {
|
||||
permissions: Permissions;
|
||||
operatePermissions?: Permissions;
|
||||
revision: Revision;
|
||||
bulletins?: BulletinEntity[];
|
||||
id: string;
|
||||
uri: string;
|
||||
component: any;
|
||||
}
|
||||
|
||||
export interface RegistryClientsState {
|
||||
registryClients: RegistryClientEntity[];
|
||||
saving: boolean;
|
||||
loadedTimestamp: string;
|
||||
error: string | null;
|
||||
status: 'pending' | 'loading' | 'error' | 'success';
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import {
|
||||
CreateRegistryClientRequest,
|
||||
CreateRegistryClientSuccess,
|
||||
DeleteRegistryClientRequest,
|
||||
DeleteRegistryClientSuccess,
|
||||
EditRegistryClientDialogRequest,
|
||||
EditRegistryClientRequest,
|
||||
EditRegistryClientRequestSuccess,
|
||||
LoadRegistryClientsResponse,
|
||||
SelectRegistryClientRequest
|
||||
} from './index';
|
||||
|
||||
export const loadRegistryClients = createAction('[Registry Clients] Load Registry Clients');
|
||||
|
||||
export const loadRegistryClientsSuccess = createAction(
|
||||
'[Registry Clients] Load Registry Clients Success',
|
||||
props<{ response: LoadRegistryClientsResponse }>()
|
||||
);
|
||||
|
||||
export const registryClientsApiError = createAction(
|
||||
'[Registry Clients] Load Registry Clients Error',
|
||||
props<{ error: string }>()
|
||||
);
|
||||
|
||||
export const openNewRegistryClientDialog = createAction('[Registry Clients] Open New Registry Client Dialog');
|
||||
|
||||
export const createRegistryClient = createAction(
|
||||
'[Registry Clients] Create Registry Client',
|
||||
props<{ request: CreateRegistryClientRequest }>()
|
||||
);
|
||||
|
||||
export const createRegistryClientSuccess = createAction(
|
||||
'[Registry Clients] Create Registry Client Success',
|
||||
props<{ response: CreateRegistryClientSuccess }>()
|
||||
);
|
||||
|
||||
export const navigateToEditRegistryClient = createAction(
|
||||
'[Registry Clients] Navigate To Edit Registry Client',
|
||||
props<{ id: string }>()
|
||||
);
|
||||
|
||||
export const openConfigureRegistryClientDialog = createAction(
|
||||
'[Registry Clients] Open Configure Registry Client Dialog',
|
||||
props<{ request: EditRegistryClientDialogRequest }>()
|
||||
);
|
||||
|
||||
export const configureRegistryClient = createAction(
|
||||
'[Registry Clients] Configure Registry Client',
|
||||
props<{ request: EditRegistryClientRequest }>()
|
||||
);
|
||||
|
||||
export const configureRegistryClientSuccess = createAction(
|
||||
'[Registry Clients] Configure Registry Client Success',
|
||||
props<{ response: EditRegistryClientRequestSuccess }>()
|
||||
);
|
||||
|
||||
export const promptRegistryClientDeletion = createAction(
|
||||
'[Registry Clients] Prompt Registry Client Deletion',
|
||||
props<{ request: DeleteRegistryClientRequest }>()
|
||||
);
|
||||
|
||||
export const deleteRegistryClient = createAction(
|
||||
'[Registry Clients] Delete Registry Client',
|
||||
props<{ request: DeleteRegistryClientRequest }>()
|
||||
);
|
||||
|
||||
export const deleteRegistryClientSuccess = createAction(
|
||||
'[Registry Clients] Delete Registry Client Success',
|
||||
props<{ response: DeleteRegistryClientSuccess }>()
|
||||
);
|
||||
|
||||
export const selectClient = createAction(
|
||||
'[Registry Clients] Select Registry Client',
|
||||
props<{ request: SelectRegistryClientRequest }>()
|
||||
);
|
|
@ -0,0 +1,422 @@
|
|||
/*
|
||||
* 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 { Injectable } from '@angular/core';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import * as RegistryClientsActions from './registry-clients.actions';
|
||||
import { catchError, from, map, NEVER, Observable, of, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { selectRegistryClientTypes } from '../../../../state/extension-types/extension-types.selectors';
|
||||
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
|
||||
import { Router } from '@angular/router';
|
||||
import { RegistryClientService } from '../../service/registry-client.service';
|
||||
import { CreateRegistryClient } from '../../ui/registry-clients/create-registry-client/create-registry-client.component';
|
||||
import { selectSaving } from './registry-clients.selectors';
|
||||
import { EditRegistryClient } from '../../ui/registry-clients/edit-registry-client/edit-registry-client.component';
|
||||
import {
|
||||
InlineServiceCreationRequest,
|
||||
InlineServiceCreationResponse,
|
||||
NewPropertyDialogRequest,
|
||||
NewPropertyDialogResponse,
|
||||
Property,
|
||||
PropertyDescriptor
|
||||
} from '../../../../state/shared';
|
||||
import { NewPropertyDialog } from '../../../../ui/common/new-property-dialog/new-property-dialog.component';
|
||||
import { ExtensionTypesService } from '../../../../service/extension-types.service';
|
||||
import { CreateControllerService } from '../../../../ui/common/controller-service/create-controller-service/create-controller-service.component';
|
||||
import { ManagementControllerServiceService } from '../../service/management-controller-service.service';
|
||||
import { Client } from '../../../../service/client.service';
|
||||
|
||||
@Injectable()
|
||||
export class RegistryClientsEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private store: Store<NiFiState>,
|
||||
private client: Client,
|
||||
private registryClientService: RegistryClientService,
|
||||
private extensionTypesService: ExtensionTypesService,
|
||||
private managementControllerServiceService: ManagementControllerServiceService,
|
||||
private dialog: MatDialog,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
loadRegistryClients$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.loadRegistryClients),
|
||||
switchMap(() =>
|
||||
from(this.registryClientService.getRegistryClients()).pipe(
|
||||
map((response) =>
|
||||
RegistryClientsActions.loadRegistryClientsSuccess({
|
||||
response: {
|
||||
registryClients: response.registries,
|
||||
loadedTimestamp: response.currentTime
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
RegistryClientsActions.registryClientsApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
openNewRegistryClientDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.openNewRegistryClientDialog),
|
||||
withLatestFrom(this.store.select(selectRegistryClientTypes)),
|
||||
tap(([action, registryClientTypes]) => {
|
||||
const dialogReference = this.dialog.open(CreateRegistryClient, {
|
||||
data: {
|
||||
registryClientTypes
|
||||
},
|
||||
panelClass: 'medium-dialog'
|
||||
});
|
||||
|
||||
dialogReference.componentInstance.saving$ = this.store.select(selectSaving);
|
||||
|
||||
dialogReference.componentInstance.createRegistryClient.pipe(take(1)).subscribe((request) => {
|
||||
this.store.dispatch(
|
||||
RegistryClientsActions.createRegistryClient({
|
||||
request
|
||||
})
|
||||
);
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
createRegistryClient$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.createRegistryClient),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.registryClientService.createRegistryClient(request)).pipe(
|
||||
map((response) =>
|
||||
RegistryClientsActions.createRegistryClientSuccess({
|
||||
response: {
|
||||
registryClient: response
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
RegistryClientsActions.registryClientsApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
createRegistryClientSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.createRegistryClientSuccess),
|
||||
map((action) => action.response),
|
||||
tap(() => {
|
||||
this.dialog.closeAll();
|
||||
}),
|
||||
switchMap((response) =>
|
||||
of(
|
||||
RegistryClientsActions.selectClient({
|
||||
request: {
|
||||
id: response.registryClient.id
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
navigateToEditRegistryClient$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.navigateToEditRegistryClient),
|
||||
map((action) => action.id),
|
||||
tap((id) => {
|
||||
this.router.navigate(['/settings', 'registry-clients', id, 'edit']);
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
openConfigureControllerServiceDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.openConfigureRegistryClientDialog),
|
||||
map((action) => action.request),
|
||||
tap((request) => {
|
||||
const registryClientId: string = request.registryClient.id;
|
||||
|
||||
const editDialogReference = this.dialog.open(EditRegistryClient, {
|
||||
data: request,
|
||||
panelClass: 'large-dialog'
|
||||
});
|
||||
|
||||
editDialogReference.componentInstance.saving$ = this.store.select(selectSaving);
|
||||
|
||||
editDialogReference.componentInstance.createNewProperty = (
|
||||
existingProperties: string[],
|
||||
allowsSensitive: boolean
|
||||
): Observable<Property> => {
|
||||
const dialogRequest: NewPropertyDialogRequest = { existingProperties, allowsSensitive };
|
||||
const newPropertyDialogReference = this.dialog.open(NewPropertyDialog, {
|
||||
data: dialogRequest,
|
||||
panelClass: 'small-dialog'
|
||||
});
|
||||
|
||||
return newPropertyDialogReference.componentInstance.newProperty.pipe(
|
||||
take(1),
|
||||
switchMap((dialogResponse: NewPropertyDialogResponse) => {
|
||||
return this.registryClientService
|
||||
.getPropertyDescriptor(
|
||||
registryClientId,
|
||||
dialogResponse.name,
|
||||
dialogResponse.sensitive
|
||||
)
|
||||
.pipe(
|
||||
take(1),
|
||||
map((response) => {
|
||||
newPropertyDialogReference.close();
|
||||
|
||||
return {
|
||||
property: dialogResponse.name,
|
||||
value: null,
|
||||
descriptor: response.propertyDescriptor
|
||||
};
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
editDialogReference.componentInstance.getServiceLink = (serviceId: string) => {
|
||||
return of(['/settings', 'management-controller-services', serviceId]);
|
||||
};
|
||||
|
||||
editDialogReference.componentInstance.createNewService = (
|
||||
request: InlineServiceCreationRequest
|
||||
): Observable<InlineServiceCreationResponse> => {
|
||||
const descriptor: PropertyDescriptor = request.descriptor;
|
||||
|
||||
// fetch all services that implement the requested service api
|
||||
return this.extensionTypesService
|
||||
.getImplementingControllerServiceTypes(
|
||||
// @ts-ignore
|
||||
descriptor.identifiesControllerService,
|
||||
descriptor.identifiesControllerServiceBundle
|
||||
)
|
||||
.pipe(
|
||||
take(1),
|
||||
switchMap((implementingTypesResponse) => {
|
||||
// show the create controller service dialog with the types that implemented the interface
|
||||
const createServiceDialogReference = this.dialog.open(CreateControllerService, {
|
||||
data: {
|
||||
controllerServiceTypes: implementingTypesResponse.controllerServiceTypes
|
||||
},
|
||||
panelClass: 'medium-dialog'
|
||||
});
|
||||
|
||||
return createServiceDialogReference.componentInstance.createControllerService.pipe(
|
||||
take(1),
|
||||
switchMap((controllerServiceType) => {
|
||||
// typically this sequence would be implemented with ngrx actions, however we are
|
||||
// currently in an edit session and we need to return both the value (new service id)
|
||||
// and updated property descriptor so the table renders correctly
|
||||
return this.managementControllerServiceService
|
||||
.createControllerService({
|
||||
revision: {
|
||||
clientId: this.client.getClientId(),
|
||||
version: 0
|
||||
},
|
||||
controllerServiceType: controllerServiceType.type,
|
||||
controllerServiceBundle: controllerServiceType.bundle
|
||||
})
|
||||
.pipe(
|
||||
take(1),
|
||||
switchMap((createReponse) => {
|
||||
// fetch an updated property descriptor
|
||||
return this.registryClientService
|
||||
.getPropertyDescriptor(
|
||||
registryClientId,
|
||||
descriptor.name,
|
||||
false
|
||||
)
|
||||
.pipe(
|
||||
take(1),
|
||||
map((descriptorResponse) => {
|
||||
createServiceDialogReference.close();
|
||||
|
||||
return {
|
||||
value: createReponse.id,
|
||||
descriptor:
|
||||
descriptorResponse.propertyDescriptor
|
||||
};
|
||||
})
|
||||
);
|
||||
}),
|
||||
catchError((error) => {
|
||||
// TODO - show error
|
||||
return NEVER;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
editDialogReference.componentInstance.editRegistryClient
|
||||
.pipe(takeUntil(editDialogReference.afterClosed()))
|
||||
.subscribe((payload: any) => {
|
||||
this.store.dispatch(
|
||||
RegistryClientsActions.configureRegistryClient({
|
||||
request: {
|
||||
id: registryClientId,
|
||||
uri: request.registryClient.uri,
|
||||
payload
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
editDialogReference.afterClosed().subscribe((response) => {
|
||||
if (response != 'ROUTED') {
|
||||
this.store.dispatch(
|
||||
RegistryClientsActions.selectClient({
|
||||
request: {
|
||||
id: registryClientId
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
configureControllerService$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.configureRegistryClient),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.registryClientService.updateRegistryClient(request)).pipe(
|
||||
map((response) =>
|
||||
RegistryClientsActions.configureRegistryClientSuccess({
|
||||
response: {
|
||||
id: request.id,
|
||||
registryClient: response
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
RegistryClientsActions.registryClientsApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
configureRegistryClientSuccess = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.configureRegistryClientSuccess),
|
||||
tap(() => {
|
||||
this.dialog.closeAll();
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
promptRegistryClientDeletion$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.promptRegistryClientDeletion),
|
||||
map((action) => action.request),
|
||||
tap((request) => {
|
||||
const dialogReference = this.dialog.open(YesNoDialog, {
|
||||
data: {
|
||||
title: 'Delete Registry Client',
|
||||
message: `Delete registry client ${request.registryClient.component.name}?`
|
||||
},
|
||||
panelClass: 'small-dialog'
|
||||
});
|
||||
|
||||
dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
|
||||
this.store.dispatch(
|
||||
RegistryClientsActions.deleteRegistryClient({
|
||||
request
|
||||
})
|
||||
);
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
deleteRegistryClient$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.deleteRegistryClient),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.registryClientService.deleteRegistryClient(request)).pipe(
|
||||
map((response) =>
|
||||
RegistryClientsActions.deleteRegistryClientSuccess({
|
||||
response: {
|
||||
registryClient: response
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
RegistryClientsActions.registryClientsApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
selectRegistryClient$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RegistryClientsActions.selectClient),
|
||||
map((action) => action.request),
|
||||
tap((request) => {
|
||||
this.router.navigate(['/settings', 'registry-clients', request.id]);
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { createReducer, on } from '@ngrx/store';
|
||||
import { produce } from 'immer';
|
||||
import { RegistryClientsState } from './index';
|
||||
import {
|
||||
configureRegistryClient,
|
||||
configureRegistryClientSuccess,
|
||||
createRegistryClient,
|
||||
createRegistryClientSuccess,
|
||||
deleteRegistryClient,
|
||||
deleteRegistryClientSuccess,
|
||||
loadRegistryClients,
|
||||
loadRegistryClientsSuccess,
|
||||
registryClientsApiError
|
||||
} from './registry-clients.actions';
|
||||
|
||||
export const initialState: RegistryClientsState = {
|
||||
registryClients: [],
|
||||
saving: false,
|
||||
loadedTimestamp: '',
|
||||
error: null,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
export const registryClientsReducer = createReducer(
|
||||
initialState,
|
||||
on(loadRegistryClients, (state) => ({
|
||||
...state,
|
||||
status: 'loading' as const
|
||||
})),
|
||||
on(loadRegistryClientsSuccess, (state, { response }) => ({
|
||||
...state,
|
||||
registryClients: response.registryClients,
|
||||
loadedTimestamp: response.loadedTimestamp,
|
||||
error: null,
|
||||
status: 'success' as const
|
||||
})),
|
||||
on(registryClientsApiError, (state, { error }) => ({
|
||||
...state,
|
||||
saving: false,
|
||||
error,
|
||||
status: 'error' as const
|
||||
})),
|
||||
on(createRegistryClient, configureRegistryClient, deleteRegistryClient, (state, { request }) => ({
|
||||
...state,
|
||||
saving: true
|
||||
})),
|
||||
on(createRegistryClientSuccess, (state, { response }) => {
|
||||
return produce(state, (draftState) => {
|
||||
draftState.registryClients.push(response.registryClient);
|
||||
draftState.saving = false;
|
||||
});
|
||||
}),
|
||||
on(configureRegistryClientSuccess, (state, { response }) => {
|
||||
return produce(state, (draftState) => {
|
||||
const componentIndex: number = draftState.registryClients.findIndex((f: any) => response.id === f.id);
|
||||
if (componentIndex > -1) {
|
||||
draftState.registryClients[componentIndex] = response.registryClient;
|
||||
}
|
||||
draftState.saving = false;
|
||||
});
|
||||
}),
|
||||
on(deleteRegistryClientSuccess, (state, { response }) => {
|
||||
return produce(state, (draftState) => {
|
||||
const componentIndex: number = draftState.registryClients.findIndex(
|
||||
(f: any) => response.registryClient.id === f.id
|
||||
);
|
||||
if (componentIndex > -1) {
|
||||
draftState.registryClients.splice(componentIndex, 1);
|
||||
}
|
||||
draftState.saving = false;
|
||||
});
|
||||
})
|
||||
);
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { createSelector } from '@ngrx/store';
|
||||
import { selectSettingsState, SettingsState } from '../index';
|
||||
import { selectCurrentRoute } from '../../../../state/router/router.selectors';
|
||||
import { RegistryClientEntity, registryClientsFeatureKey, RegistryClientsState } from './index';
|
||||
|
||||
export const selectRegistryClientsState = createSelector(
|
||||
selectSettingsState,
|
||||
(state: SettingsState) => state[registryClientsFeatureKey]
|
||||
);
|
||||
|
||||
export const selectSaving = createSelector(selectRegistryClientsState, (state: RegistryClientsState) => state.saving);
|
||||
|
||||
export const selectRegistryClientIdFromRoute = createSelector(selectCurrentRoute, (route) => {
|
||||
if (route) {
|
||||
// always select the registry client from the route
|
||||
return route.params.id;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
export const selectSingleEditedRegistryClient = createSelector(selectCurrentRoute, (route) => {
|
||||
if (route?.routeConfig?.path == 'edit') {
|
||||
return route.params.id;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
export const selectRegistryClients = createSelector(
|
||||
selectRegistryClientsState,
|
||||
(state: RegistryClientsState) => state.registryClients
|
||||
);
|
||||
|
||||
export const selectRegistryClient = (id: string) =>
|
||||
createSelector(selectRegistryClients, (entities: RegistryClientEntity[]) =>
|
||||
entities.find((entity) => id == entity.id)
|
||||
);
|
|
@ -105,15 +105,23 @@ export class ReportingTasksEffects {
|
|||
)
|
||||
);
|
||||
|
||||
createReportingTaskSuccess$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ReportingTaskActions.createReportingTaskSuccess),
|
||||
tap(() => {
|
||||
this.dialog.closeAll();
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
createReportingTaskSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ReportingTaskActions.createReportingTaskSuccess),
|
||||
map((action) => action.response),
|
||||
tap(() => {
|
||||
this.dialog.closeAll();
|
||||
}),
|
||||
switchMap((response) =>
|
||||
of(
|
||||
ReportingTaskActions.selectReportingTask({
|
||||
request: {
|
||||
reportingTask: response.reportingTask
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
promptReportingTaskDeletion$ = createEffect(
|
||||
|
@ -124,8 +132,8 @@ export class ReportingTasksEffects {
|
|||
tap((request) => {
|
||||
const dialogReference = this.dialog.open(YesNoDialog, {
|
||||
data: {
|
||||
title: 'Delete Controller Service',
|
||||
message: `Delete controller service ${request.reportingTask.component.name}?`
|
||||
title: 'Delete Reporting Task',
|
||||
message: `Delete reporting task ${request.reportingTask.component.name}?`
|
||||
},
|
||||
panelClass: 'small-dialog'
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ import { ReportingTasksState } from './index';
|
|||
import {
|
||||
createReportingTask,
|
||||
createReportingTaskSuccess,
|
||||
deleteReportingTask,
|
||||
deleteReportingTaskSuccess,
|
||||
loadReportingTasks,
|
||||
loadReportingTasksSuccess,
|
||||
|
@ -56,7 +57,7 @@ export const reportingTasksReducer = createReducer(
|
|||
error,
|
||||
status: 'error' as const
|
||||
})),
|
||||
on(createReportingTask, (state, { request }) => ({
|
||||
on(createReportingTask, deleteReportingTask, (state, { request }) => ({
|
||||
...state,
|
||||
saving: true
|
||||
})),
|
||||
|
@ -94,6 +95,7 @@ export const reportingTasksReducer = createReducer(
|
|||
if (componentIndex > -1) {
|
||||
draftState.reportingTasks.splice(componentIndex, 1);
|
||||
}
|
||||
draftState.saving = false;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
@ -16,14 +16,18 @@
|
|||
-->
|
||||
|
||||
<div class="general-form w-96">
|
||||
<form [formGroup]="controllerForm">
|
||||
<form [formGroup]="controllerForm" *ngIf="currentUser$ | async; let currentUser">
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<mat-label>Maximum Timer Driven Thread Count</mat-label>
|
||||
<input matInput formControlName="timerDrivenThreadCount" type="text" />
|
||||
<input
|
||||
matInput
|
||||
formControlName="timerDrivenThreadCount"
|
||||
[readonly]="!currentUser.controllerPermissions.canWrite"
|
||||
type="text" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<div *ngIf="currentUser.controllerPermissions.canWrite">
|
||||
<button
|
||||
[disabled]="!controllerForm.dirty || controllerForm.invalid"
|
||||
type="button"
|
||||
|
|
|
@ -21,6 +21,7 @@ import { ControllerEntity, GeneralState, UpdateControllerConfigRequest } from '.
|
|||
import { Store } from '@ngrx/store';
|
||||
import { updateControllerConfig } from '../../../state/general/general.actions';
|
||||
import { Client } from '../../../../../service/client.service';
|
||||
import { selectUser } from '../../../../../state/user/user.selectors';
|
||||
|
||||
@Component({
|
||||
selector: 'general-form',
|
||||
|
@ -35,6 +36,7 @@ export class GeneralForm {
|
|||
this.controllerForm.get('timerDrivenThreadCount')?.setValue(controller.component.maxTimerDrivenThreadCount);
|
||||
}
|
||||
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
controllerForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
|
||||
</div>
|
||||
<ng-template #loaded>
|
||||
<div class="flex flex-col h-full gap-y-2">
|
||||
<div class="flex justify-end">
|
||||
<div class="flex flex-col h-full gap-y-2" *ngIf="currentUser$ | async; let currentUser">
|
||||
<div class="flex justify-end" *ngIf="currentUser.controllerPermissions.canWrite">
|
||||
<button class="nifi-button" (click)="openNewControllerServiceDialog()">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
|
|
|
@ -36,6 +36,8 @@ import { ControllerServiceEntity } from '../../../../state/shared';
|
|||
import { initialState } from '../../state/management-controller-services/management-controller-services.reducer';
|
||||
import { filter, switchMap, take } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import { NiFiState } from '../../../../state';
|
||||
|
||||
@Component({
|
||||
selector: 'management-controller-services',
|
||||
|
@ -45,8 +47,9 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|||
export class ManagementControllerServices implements OnInit {
|
||||
serviceState$ = this.store.select(selectManagementControllerServicesState);
|
||||
selectedServiceId$ = this.store.select(selectControllerServiceIdFromRoute);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
|
||||
constructor(private store: Store<ManagementControllerServicesState>) {
|
||||
constructor(private store: Store<NiFiState>) {
|
||||
this.store
|
||||
.select(selectSingleEditedService)
|
||||
.pipe(
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<!--
|
||||
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
~ contributor license agreements. See the NOTICE file distributed with
|
||||
~ this work for additional information regarding copyright ownership.
|
||||
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
~ (the "License"); you may not use this file except in compliance with
|
||||
~ the License. You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title>Add Registry Client</h2>
|
||||
<form class="create-registry-client-form" [formGroup]="createRegistryClientForm">
|
||||
<mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" type="text" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select formControlName="type">
|
||||
<ng-container *ngFor="let option of request.registryClientTypes">
|
||||
<ng-container *ngIf="option.description; else noDescription">
|
||||
<mat-option
|
||||
[value]="option.type"
|
||||
nifiTooltip
|
||||
[tooltipComponentType]="TextTip"
|
||||
[tooltipInputData]="getOptionTipData(option)"
|
||||
[delayClose]="false">
|
||||
{{ formatType(option) }}
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
<ng-template #noDescription>
|
||||
<mat-option [value]="option.type">
|
||||
{{ formatType(option) }}
|
||||
</mat-option>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<mat-label>Description</mat-label>
|
||||
<textarea matInput formControlName="description" type="text"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as saving">
|
||||
<button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
|
||||
<button
|
||||
[disabled]="!createRegistryClientForm.dirty || createRegistryClientForm.invalid || saving.value"
|
||||
type="button"
|
||||
color="primary"
|
||||
(click)="createRegistryClientClicked()"
|
||||
mat-raised-button>
|
||||
<span *nifiSpinner="saving.value">Apply</span>
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.create-registry-client-form {
|
||||
@include mat.button-density(-1);
|
||||
|
||||
.mat-mdc-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { CreateRegistryClient } from './create-registry-client.component';
|
||||
import { CreateRegistryClientDialogRequest } from '../../../state/registry-clients';
|
||||
|
||||
describe('CreateRegistryClient', () => {
|
||||
let component: CreateRegistryClient;
|
||||
let fixture: ComponentFixture<CreateRegistryClient>;
|
||||
|
||||
const data: CreateRegistryClientDialogRequest = {
|
||||
registryClientTypes: [
|
||||
{
|
||||
type: 'org.apache.nifi.registry.flow.NifiRegistryFlowRegistryClient',
|
||||
bundle: {
|
||||
group: 'org.apache.nifi',
|
||||
artifact: 'nifi-flow-registry-client-nar',
|
||||
version: '2.0.0-SNAPSHOT'
|
||||
},
|
||||
restricted: false,
|
||||
tags: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CreateRegistryClient, BrowserAnimationsModule],
|
||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
|
||||
});
|
||||
fixture = TestBed.createComponent(CreateRegistryClient);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Inject, Input, Output } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DocumentedType, TextTipInput } from '../../../../../state/shared';
|
||||
import { CreateRegistryClientDialogRequest, CreateRegistryClientRequest } from '../../../state/registry-clients';
|
||||
import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
|
||||
import { Client } from '../../../../../service/client.service';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive';
|
||||
import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
|
||||
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||
|
||||
@Component({
|
||||
selector: 'create-registry-client',
|
||||
standalone: true,
|
||||
templateUrl: './create-registry-client.component.html',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
NifiSpinnerDirective,
|
||||
MatSelectModule,
|
||||
NgForOf,
|
||||
NifiTooltipDirective
|
||||
],
|
||||
styleUrls: ['./create-registry-client.component.scss']
|
||||
})
|
||||
export class CreateRegistryClient {
|
||||
@Input() saving$!: Observable<boolean>;
|
||||
@Output() createRegistryClient: EventEmitter<CreateRegistryClientRequest> =
|
||||
new EventEmitter<CreateRegistryClientRequest>();
|
||||
|
||||
protected readonly TextTip = TextTip;
|
||||
|
||||
createRegistryClientForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public request: CreateRegistryClientDialogRequest,
|
||||
private formBuilder: FormBuilder,
|
||||
private nifiCommon: NiFiCommon,
|
||||
private client: Client
|
||||
) {
|
||||
let type: string | null = null;
|
||||
if (request.registryClientTypes.length > 0) {
|
||||
type = request.registryClientTypes[0].type;
|
||||
}
|
||||
|
||||
// build the form
|
||||
this.createRegistryClientForm = this.formBuilder.group({
|
||||
name: new FormControl('', Validators.required),
|
||||
type: new FormControl(type, Validators.required),
|
||||
description: new FormControl('')
|
||||
});
|
||||
}
|
||||
|
||||
formatType(option: DocumentedType): string {
|
||||
return this.nifiCommon.substringAfterLast(option.type, '.');
|
||||
}
|
||||
|
||||
getOptionTipData(option: DocumentedType): TextTipInput {
|
||||
return {
|
||||
// @ts-ignore
|
||||
text: option.description
|
||||
};
|
||||
}
|
||||
|
||||
createRegistryClientClicked() {
|
||||
const request: CreateRegistryClientRequest = {
|
||||
revision: {
|
||||
clientId: this.client.getClientId(),
|
||||
version: 0
|
||||
},
|
||||
component: {
|
||||
name: this.createRegistryClientForm.get('name')?.value,
|
||||
type: this.createRegistryClientForm.get('type')?.value,
|
||||
description: this.createRegistryClientForm.get('description')?.value
|
||||
}
|
||||
};
|
||||
|
||||
this.createRegistryClient.next(request);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<!--
|
||||
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
~ contributor license agreements. See the NOTICE file distributed with
|
||||
~ this work for additional information regarding copyright ownership.
|
||||
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
~ (the "License"); you may not use this file except in compliance with
|
||||
~ the License. You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title>Edit Registry Client</h2>
|
||||
<form class="edit-registry-client-form" [formGroup]="editRegistryClientForm">
|
||||
<mat-dialog-content>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Settings">
|
||||
<div class="tab-content py-4 flex flex-col">
|
||||
<div class="flex flex-col mb-5">
|
||||
<div>Id</div>
|
||||
<div class="value">{{ request.registryClient.id }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" type="text" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="flex flex-col mb-5">
|
||||
<div>Id</div>
|
||||
<div class="value">{{ request.registryClient.component.type }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<mat-label>Description</mat-label>
|
||||
<textarea matInput formControlName="description" type="text"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="Properties">
|
||||
<div class="tab-content py-4">
|
||||
<property-table
|
||||
formControlName="properties"
|
||||
[createNewProperty]="createNewProperty"
|
||||
[createNewService]="createNewService"
|
||||
[getParameters]="getParameters"
|
||||
[getServiceLink]="getServiceLink"
|
||||
[supportsSensitiveDynamicProperties]="
|
||||
request.registryClient.component.supportsSensitiveDynamicProperties
|
||||
"></property-table>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as saving">
|
||||
<button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
|
||||
<button
|
||||
[disabled]="!editRegistryClientForm.dirty || editRegistryClientForm.invalid || saving.value"
|
||||
type="button"
|
||||
color="primary"
|
||||
(click)="createRegistryClientClicked()"
|
||||
mat-raised-button>
|
||||
<span *nifiSpinner="saving.value">Apply</span>
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.edit-registry-client-form {
|
||||
@include mat.button-density(-1);
|
||||
|
||||
.mdc-dialog__content {
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
|
||||
.tab-content {
|
||||
height: 475px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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 { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { EditRegistryClient } from './edit-registry-client.component';
|
||||
import { EditRegistryClientDialogRequest } from '../../../state/registry-clients';
|
||||
|
||||
describe('EditRegistryClient', () => {
|
||||
let component: EditRegistryClient;
|
||||
let fixture: ComponentFixture<EditRegistryClient>;
|
||||
|
||||
const data: EditRegistryClientDialogRequest = {
|
||||
registryClient: {
|
||||
revision: {
|
||||
clientId: 'fdbbc975-5fd5-4dbf-9308-432d75d20c04',
|
||||
version: 2
|
||||
},
|
||||
id: '454cab42-018c-1000-6f9f-3603643c504c',
|
||||
uri: 'https://localhost:4200/nifi-api/controller/registry-clients/454cab42-018c-1000-6f9f-3603643c504c',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: '454cab42-018c-1000-6f9f-3603643c504c',
|
||||
name: 'registry',
|
||||
description: '',
|
||||
type: 'org.apache.nifi.registry.flow.NifiRegistryFlowRegistryClient',
|
||||
bundle: {
|
||||
group: 'org.apache.nifi',
|
||||
artifact: 'nifi-flow-registry-client-nar',
|
||||
version: '2.0.0-SNAPSHOT'
|
||||
},
|
||||
properties: {
|
||||
url: null,
|
||||
'ssl-context-service': null
|
||||
},
|
||||
descriptors: {
|
||||
url: {
|
||||
name: 'url',
|
||||
displayName: 'URL',
|
||||
description: 'URL of the NiFi Registry',
|
||||
required: true,
|
||||
sensitive: false,
|
||||
dynamic: false,
|
||||
supportsEl: false,
|
||||
expressionLanguageScope: 'Not Supported',
|
||||
dependencies: []
|
||||
},
|
||||
'ssl-context-service': {
|
||||
name: 'ssl-context-service',
|
||||
displayName: 'SSL Context Service',
|
||||
description: 'Specifies the SSL Context Service to use for communicating with NiFiRegistry',
|
||||
allowableValues: [
|
||||
{
|
||||
allowableValue: {
|
||||
displayName: 'StandardRestrictedSSLContextService',
|
||||
value: '45b3d2bf-018c-1000-f99a-fd441220c9d7'
|
||||
},
|
||||
canRead: true
|
||||
}
|
||||
],
|
||||
required: false,
|
||||
sensitive: false,
|
||||
dynamic: false,
|
||||
supportsEl: false,
|
||||
expressionLanguageScope: 'Not Supported',
|
||||
identifiesControllerService: 'org.apache.nifi.ssl.SSLContextService',
|
||||
identifiesControllerServiceBundle: {
|
||||
group: 'org.apache.nifi',
|
||||
artifact: 'nifi-standard-services-api-nar',
|
||||
version: '2.0.0-SNAPSHOT'
|
||||
},
|
||||
dependencies: []
|
||||
}
|
||||
},
|
||||
supportsSensitiveDynamicProperties: false,
|
||||
restricted: false,
|
||||
deprecated: false,
|
||||
validationErrors: ["'URL' is invalid because URL is required"],
|
||||
validationStatus: 'INVALID',
|
||||
multipleVersionsAvailable: false,
|
||||
extensionMissing: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [EditRegistryClient, BrowserAnimationsModule],
|
||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
|
||||
});
|
||||
fixture = TestBed.createComponent(EditRegistryClient);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Inject, Input, Output } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
DocumentedType,
|
||||
InlineServiceCreationRequest,
|
||||
InlineServiceCreationResponse,
|
||||
Parameter,
|
||||
Property,
|
||||
TextTipInput
|
||||
} from '../../../../../state/shared';
|
||||
import { EditRegistryClientDialogRequest } from '../../../state/registry-clients';
|
||||
import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
|
||||
import { Client } from '../../../../../service/client.service';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { NifiTooltipDirective } from '../../../../../ui/common/tooltips/nifi-tooltip.directive';
|
||||
import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
|
||||
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { PropertyTable } from '../../../../../ui/common/property-table/property-table.component';
|
||||
|
||||
@Component({
|
||||
selector: 'edit-registry-client',
|
||||
standalone: true,
|
||||
templateUrl: './edit-registry-client.component.html',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
NifiSpinnerDirective,
|
||||
MatSelectModule,
|
||||
NgForOf,
|
||||
NifiTooltipDirective,
|
||||
MatTabsModule,
|
||||
PropertyTable
|
||||
],
|
||||
styleUrls: ['./edit-registry-client.component.scss']
|
||||
})
|
||||
export class EditRegistryClient {
|
||||
@Input() createNewProperty!: (existingProperties: string[], allowsSensitive: boolean) => Observable<Property>;
|
||||
@Input() createNewService!: (request: InlineServiceCreationRequest) => Observable<InlineServiceCreationResponse>;
|
||||
@Input() getParameters!: (sensitive: boolean) => Observable<Parameter[]>;
|
||||
@Input() getServiceLink!: (serviceId: string) => Observable<string[]>;
|
||||
@Input() saving$!: Observable<boolean>;
|
||||
@Output() editRegistryClient: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
protected readonly TextTip = TextTip;
|
||||
|
||||
editRegistryClientForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public request: EditRegistryClientDialogRequest,
|
||||
private formBuilder: FormBuilder,
|
||||
private nifiCommon: NiFiCommon,
|
||||
private client: Client
|
||||
) {
|
||||
const serviceProperties: any = request.registryClient.component.properties;
|
||||
const properties: Property[] = Object.entries(serviceProperties).map((entry: any) => {
|
||||
const [property, value] = entry;
|
||||
return {
|
||||
property,
|
||||
value,
|
||||
descriptor: request.registryClient.component.descriptors[property]
|
||||
};
|
||||
});
|
||||
|
||||
// build the form
|
||||
this.editRegistryClientForm = this.formBuilder.group({
|
||||
name: new FormControl(request.registryClient.component.name, Validators.required),
|
||||
description: new FormControl(request.registryClient.component.description),
|
||||
properties: new FormControl(properties)
|
||||
});
|
||||
}
|
||||
|
||||
formatType(option: DocumentedType): string {
|
||||
return this.nifiCommon.substringAfterLast(option.type, '.');
|
||||
}
|
||||
|
||||
getOptionTipData(option: DocumentedType): TextTipInput {
|
||||
return {
|
||||
// @ts-ignore
|
||||
text: option.description
|
||||
};
|
||||
}
|
||||
|
||||
createRegistryClientClicked() {
|
||||
const payload: any = {
|
||||
revision: this.client.getRevision(this.request.registryClient),
|
||||
component: {
|
||||
id: this.request.registryClient.id,
|
||||
name: this.editRegistryClientForm.get('name')?.value,
|
||||
type: this.editRegistryClientForm.get('type')?.value,
|
||||
description: this.editRegistryClientForm.get('description')?.value
|
||||
}
|
||||
};
|
||||
|
||||
const propertyControl: AbstractControl | null = this.editRegistryClientForm.get('properties');
|
||||
if (propertyControl && propertyControl.dirty) {
|
||||
const properties: Property[] = propertyControl.value;
|
||||
const values: { [key: string]: string | null } = {};
|
||||
properties.forEach((property) => (values[property.property] = property.value));
|
||||
payload.component.properties = values;
|
||||
payload.component.sensitiveDynamicPropertyNames = properties
|
||||
.filter((property) => property.descriptor.dynamic && property.descriptor.sensitive)
|
||||
.map((property) => property.descriptor.name);
|
||||
}
|
||||
|
||||
this.editRegistryClient.next(payload);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
<!--
|
||||
~ 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="relative h-full border">
|
||||
<div class="registry-client-table listing-table absolute inset-0 overflow-y-auto">
|
||||
<table
|
||||
mat-table
|
||||
[dataSource]="dataSource"
|
||||
matSort
|
||||
matSortDisableClear
|
||||
(matSortChange)="updateSort($event)"
|
||||
[matSortActive]="sort.active"
|
||||
[matSortDirection]="sort.direction">
|
||||
<!-- More Details Column -->
|
||||
<ng-container matColumnDef="moreDetails">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<ng-container *ngIf="canRead(item)">
|
||||
<div class="flex items-center">
|
||||
<!-- TODO - handle read only in configure component? -->
|
||||
<div
|
||||
class="mr-3 pointer fa fa-warning has-errors"
|
||||
*ngIf="hasErrors(item)"
|
||||
nifiTooltip
|
||||
[tooltipComponentType]="ValidationErrorsTip"
|
||||
[tooltipInputData]="getValidationErrorsTipData(item)"></div>
|
||||
<div
|
||||
class="mr-3 pointer fa fa-sticky-note-o"
|
||||
*ngIf="hasBulletins(item)"
|
||||
nifiTooltip
|
||||
[tooltipComponentType]="BulletinsTip"
|
||||
[tooltipInputData]="getBulletinsTipData(item)"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<ng-container *ngIf="canRead(item); else nameNoPermissions">
|
||||
{{ item.component.name }}
|
||||
</ng-container>
|
||||
<ng-template #nameNoPermissions>
|
||||
<div class="unset">{{ item.id }}</div>
|
||||
</ng-template>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Description Column -->
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Description</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<ng-container *ngIf="canRead(item); else descriptionNoPermissions">
|
||||
{{ item.component.description }}
|
||||
</ng-container>
|
||||
<ng-template #descriptionNoPermissions>
|
||||
<div class="unset">{{ item.id }}</div>
|
||||
</ng-template>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Type Column -->
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<ng-container *ngIf="canRead(item)">
|
||||
{{ formatType(item) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Bundle Column -->
|
||||
<ng-container matColumnDef="bundle">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Bundle</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<ng-container *ngIf="canRead(item)">
|
||||
{{ formatBundle(item) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div
|
||||
class="pointer fa fa-pencil"
|
||||
*ngIf="canConfigure(item)"
|
||||
(click)="configureClicked(item, $event)"
|
||||
title="Configure"></div>
|
||||
<div
|
||||
class="pointer fa fa-trash"
|
||||
*ngIf="canDelete(item)"
|
||||
(click)="deleteClicked(item, $event)"
|
||||
title="Delete"></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>
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.registry-client-table.listing-table {
|
||||
table {
|
||||
.mat-column-moreDetails {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { RegistryClientTable } from './registry-client-table.component';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('RegistryClientTable', () => {
|
||||
let component: RegistryClientTable;
|
||||
let fixture: ComponentFixture<RegistryClientTable>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [RegistryClientTable],
|
||||
imports: [MatTableModule, MatSortModule, BrowserAnimationsModule]
|
||||
});
|
||||
fixture = TestBed.createComponent(RegistryClientTable);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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 { MatTableDataSource } from '@angular/material/table';
|
||||
import { Sort } from '@angular/material/sort';
|
||||
import { ReportingTaskEntity } from '../../../state/reporting-tasks';
|
||||
import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
|
||||
import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
|
||||
import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
|
||||
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||
import { BulletinsTipInput, ValidationErrorsTipInput } from '../../../../../state/shared';
|
||||
import { RegistryClientEntity } from '../../../state/registry-clients';
|
||||
|
||||
@Component({
|
||||
selector: 'registry-client-table',
|
||||
templateUrl: './registry-client-table.component.html',
|
||||
styleUrls: ['./registry-client-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
|
||||
})
|
||||
export class RegistryClientTable {
|
||||
@Input() set registryClients(registryClientEntities: RegistryClientEntity[]) {
|
||||
if (registryClientEntities) {
|
||||
this.dataSource.data = this.sortEvents(registryClientEntities, this.sort);
|
||||
}
|
||||
}
|
||||
|
||||
@Input() selectedRegistryClientId!: string;
|
||||
|
||||
@Output() selectRegistryClient: EventEmitter<RegistryClientEntity> = new EventEmitter<RegistryClientEntity>();
|
||||
@Output() configureRegistryClient: EventEmitter<RegistryClientEntity> = new EventEmitter<RegistryClientEntity>();
|
||||
@Output() deleteRegistryClient: EventEmitter<RegistryClientEntity> = new EventEmitter<RegistryClientEntity>();
|
||||
|
||||
protected readonly TextTip = TextTip;
|
||||
protected readonly BulletinsTip = BulletinsTip;
|
||||
protected readonly ValidationErrorsTip = ValidationErrorsTip;
|
||||
|
||||
displayedColumns: string[] = ['moreDetails', 'name', 'description', 'type', 'bundle', 'actions'];
|
||||
dataSource: MatTableDataSource<RegistryClientEntity> = new MatTableDataSource<RegistryClientEntity>();
|
||||
|
||||
sort: Sort = {
|
||||
active: 'name',
|
||||
direction: 'asc'
|
||||
};
|
||||
|
||||
constructor(private nifiCommon: NiFiCommon) {}
|
||||
|
||||
canRead(entity: RegistryClientEntity): boolean {
|
||||
return entity.permissions.canRead;
|
||||
}
|
||||
|
||||
canWrite(entity: RegistryClientEntity): boolean {
|
||||
return entity.permissions.canWrite;
|
||||
}
|
||||
|
||||
hasErrors(entity: RegistryClientEntity): boolean {
|
||||
return this.canRead(entity) && !this.nifiCommon.isEmpty(entity.component.validationErrors);
|
||||
}
|
||||
|
||||
getValidationErrorsTipData(entity: RegistryClientEntity): ValidationErrorsTipInput {
|
||||
return {
|
||||
isValidating: false,
|
||||
validationErrors: entity.component.validationErrors
|
||||
};
|
||||
}
|
||||
|
||||
hasBulletins(entity: RegistryClientEntity): boolean {
|
||||
return this.canRead(entity) && !this.nifiCommon.isEmpty(entity.bulletins);
|
||||
}
|
||||
|
||||
getBulletinsTipData(entity: RegistryClientEntity): BulletinsTipInput {
|
||||
return {
|
||||
// @ts-ignore
|
||||
bulletins: entity.bulletins
|
||||
};
|
||||
}
|
||||
|
||||
formatType(entity: RegistryClientEntity): string {
|
||||
return this.nifiCommon.formatType(entity.component);
|
||||
}
|
||||
|
||||
formatBundle(entity: RegistryClientEntity): string {
|
||||
return this.nifiCommon.formatBundle(entity.component.bundle);
|
||||
}
|
||||
|
||||
updateSort(sort: Sort): void {
|
||||
this.sort = sort;
|
||||
this.dataSource.data = this.sortEvents(this.dataSource.data, sort);
|
||||
}
|
||||
|
||||
sortEvents(entities: RegistryClientEntity[], sort: Sort): RegistryClientEntity[] {
|
||||
const data: RegistryClientEntity[] = entities.slice();
|
||||
return data.sort((a, b) => {
|
||||
const isAsc = sort.direction === 'asc';
|
||||
|
||||
let retVal: number = 0;
|
||||
switch (sort.active) {
|
||||
case 'name':
|
||||
retVal = this.nifiCommon.compareString(a.component.name, b.component.name);
|
||||
break;
|
||||
case 'description':
|
||||
retVal = this.nifiCommon.compareString(a.component.description, b.component.description);
|
||||
break;
|
||||
case 'type':
|
||||
retVal = this.nifiCommon.compareString(this.formatType(a), this.formatType(b));
|
||||
break;
|
||||
case 'bundle':
|
||||
retVal = this.nifiCommon.compareString(this.formatBundle(a), this.formatBundle(b));
|
||||
break;
|
||||
}
|
||||
|
||||
return retVal * (isAsc ? 1 : -1);
|
||||
});
|
||||
}
|
||||
|
||||
canConfigure(entity: RegistryClientEntity): boolean {
|
||||
return this.canRead(entity) && this.canWrite(entity);
|
||||
}
|
||||
|
||||
configureClicked(entity: RegistryClientEntity, event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
this.configureRegistryClient.next(entity);
|
||||
}
|
||||
|
||||
canDelete(entity: RegistryClientEntity): boolean {
|
||||
return this.canRead(entity) && this.canWrite(entity);
|
||||
}
|
||||
|
||||
deleteClicked(entity: RegistryClientEntity, event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
this.deleteRegistryClient.next(entity);
|
||||
}
|
||||
|
||||
select(entity: ReportingTaskEntity): void {
|
||||
this.selectRegistryClient.next(entity);
|
||||
}
|
||||
|
||||
isSelected(entity: ReportingTaskEntity): boolean {
|
||||
if (this.selectedRegistryClientId) {
|
||||
return entity.id == this.selectedRegistryClientId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -15,4 +15,34 @@
|
|||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<p>registry-clients works!</p>
|
||||
<ng-container *ngIf="registryClientsState$ | async; let registryClientState">
|
||||
<div *ngIf="isInitialLoading(registryClientState); else loaded">
|
||||
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
|
||||
</div>
|
||||
<ng-template #loaded>
|
||||
<div class="flex flex-col h-full gap-y-2" *ngIf="currentUser$ | async; let currentUser">
|
||||
<div class="flex justify-end" *ngIf="currentUser.controllerPermissions.canWrite">
|
||||
<button class="nifi-button" (click)="openNewRegistryClientDialog()">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<registry-client-table
|
||||
[selectedRegistryClientId]="selectedRegistryClientId$ | async"
|
||||
[registryClients]="registryClientState.registryClients"
|
||||
(selectRegistryClient)="selectRegistryClient($event)"
|
||||
(configureRegistryClient)="configureRegistryClient($event)"
|
||||
(deleteRegistryClient)="deleteRegistryClient($event)"></registry-client-table>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="refresh-container flex items-center gap-x-2">
|
||||
<button class="nifi-button" (click)="refreshRegistryClientListing()">
|
||||
<i class="fa fa-refresh" [class.fa-spin]="registryClientState.status === 'loading'"></i>
|
||||
</button>
|
||||
<div>Last updated:</div>
|
||||
<div class="refresh-timestamp">{{ registryClientState.loadedTimestamp }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RegistryClients } from './registry-clients.component';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../state/registry-clients/registry-clients.reducer';
|
||||
|
||||
describe('RegistryClients', () => {
|
||||
let component: RegistryClients;
|
||||
|
@ -25,7 +27,8 @@ describe('RegistryClients', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [RegistryClients]
|
||||
declarations: [RegistryClients],
|
||||
providers: [provideMockStore({ initialState })]
|
||||
});
|
||||
fixture = TestBed.createComponent(RegistryClients);
|
||||
component = fixture.componentInstance;
|
||||
|
|
|
@ -15,11 +15,107 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
selectRegistryClient,
|
||||
selectRegistryClientIdFromRoute,
|
||||
selectRegistryClientsState,
|
||||
selectSingleEditedRegistryClient
|
||||
} from '../../state/registry-clients/registry-clients.selectors';
|
||||
import {
|
||||
loadRegistryClients,
|
||||
navigateToEditRegistryClient,
|
||||
openConfigureRegistryClientDialog,
|
||||
openNewRegistryClientDialog,
|
||||
promptRegistryClientDeletion,
|
||||
selectClient
|
||||
} from '../../state/registry-clients/registry-clients.actions';
|
||||
import { RegistryClientEntity, RegistryClientsState } from '../../state/registry-clients';
|
||||
import { initialState } from '../../state/registry-clients/registry-clients.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { filter, switchMap, take } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'registry-clients',
|
||||
templateUrl: './registry-clients.component.html',
|
||||
styleUrls: ['./registry-clients.component.scss']
|
||||
})
|
||||
export class RegistryClients {}
|
||||
export class RegistryClients implements OnInit {
|
||||
registryClientsState$ = this.store.select(selectRegistryClientsState);
|
||||
selectedRegistryClientId$ = this.store.select(selectRegistryClientIdFromRoute);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
|
||||
constructor(private store: Store<NiFiState>) {
|
||||
this.store
|
||||
.select(selectSingleEditedRegistryClient)
|
||||
.pipe(
|
||||
filter((id: string) => id != null),
|
||||
switchMap((id: string) =>
|
||||
this.store.select(selectRegistryClient(id)).pipe(
|
||||
filter((entity) => entity != null),
|
||||
take(1)
|
||||
)
|
||||
),
|
||||
takeUntilDestroyed()
|
||||
)
|
||||
.subscribe((entity) => {
|
||||
if (entity) {
|
||||
this.store.dispatch(
|
||||
openConfigureRegistryClientDialog({
|
||||
request: {
|
||||
registryClient: entity
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(loadRegistryClients());
|
||||
}
|
||||
|
||||
isInitialLoading(state: RegistryClientsState): boolean {
|
||||
// using the current timestamp to detect the initial load event
|
||||
return state.loadedTimestamp == initialState.loadedTimestamp;
|
||||
}
|
||||
|
||||
openNewRegistryClientDialog(): void {
|
||||
this.store.dispatch(openNewRegistryClientDialog());
|
||||
}
|
||||
|
||||
refreshRegistryClientListing(): void {
|
||||
this.store.dispatch(loadRegistryClients());
|
||||
}
|
||||
|
||||
selectRegistryClient(entity: RegistryClientEntity): void {
|
||||
this.store.dispatch(
|
||||
selectClient({
|
||||
request: {
|
||||
id: entity.id
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
configureRegistryClient(entity: RegistryClientEntity): void {
|
||||
this.store.dispatch(
|
||||
navigateToEditRegistryClient({
|
||||
id: entity.id
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteRegistryClient(entity: RegistryClientEntity): void {
|
||||
this.store.dispatch(
|
||||
promptRegistryClientDeletion({
|
||||
request: {
|
||||
registryClient: entity
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,15 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RegistryClients } from './registry-clients.component';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { RegistryClientTable } from './registry-client-table/registry-client-table.component';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RegistryClients],
|
||||
declarations: [RegistryClients, RegistryClientTable],
|
||||
exports: [RegistryClients],
|
||||
imports: [CommonModule]
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule, MatTableModule, MatSortModule, NifiTooltipDirective]
|
||||
})
|
||||
export class RegistryClientsModule {}
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
|
||||
</div>
|
||||
<ng-template #loaded>
|
||||
<div class="flex flex-col h-full gap-y-2">
|
||||
<div class="flex justify-end">
|
||||
<div class="flex flex-col h-full gap-y-2" *ngIf="currentUser$ | async; let currentUser">
|
||||
<div class="flex justify-end" *ngIf="currentUser.controllerPermissions.canWrite">
|
||||
<button class="nifi-button" (click)="openNewReportingTaskDialog()">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
|
|
|
@ -31,6 +31,8 @@ import {
|
|||
stopReportingTask
|
||||
} from '../../state/reporting-tasks/reporting-tasks.actions';
|
||||
import { initialState } from '../../state/reporting-tasks/reporting-tasks.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import { NiFiState } from '../../../../state';
|
||||
|
||||
@Component({
|
||||
selector: 'reporting-tasks',
|
||||
|
@ -40,8 +42,9 @@ import { initialState } from '../../state/reporting-tasks/reporting-tasks.reduce
|
|||
export class ReportingTasks implements OnInit {
|
||||
reportingTaskState$ = this.store.select(selectReportingTasksState);
|
||||
selectedReportingTaskId$ = this.store.select(selectReportingTaskIdFromRoute);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
|
||||
constructor(private store: Store<ReportingTasksState>) {}
|
||||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(loadReportingTasks());
|
||||
|
|
|
@ -48,6 +48,10 @@ export class ExtensionTypesService {
|
|||
return this.httpClient.get(`${ExtensionTypesService.API}/flow/reporting-task-types`);
|
||||
}
|
||||
|
||||
getRegistryClientTypes(): Observable<any> {
|
||||
return this.httpClient.get(`${ExtensionTypesService.API}/controller/registry-types`);
|
||||
}
|
||||
|
||||
getPrioritizers(): Observable<any> {
|
||||
return this.httpClient.get(`${ExtensionTypesService.API}/flow/prioritizers`);
|
||||
}
|
||||
|
|
|
@ -18,11 +18,11 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanMatchFn } from '@angular/router';
|
||||
|
||||
import { authGuard } from './auth.guard';
|
||||
import { authenticationGuard } from './authentication.guard';
|
||||
|
||||
describe('authGuard', () => {
|
||||
describe('authenticationGuard', () => {
|
||||
const executeGuard: CanMatchFn = (...guardParameters) =>
|
||||
TestBed.runInInjectionContext(() => authGuard(...guardParameters));
|
||||
TestBed.runInInjectionContext(() => authenticationGuard(...guardParameters));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
|
@ -26,7 +26,7 @@ import { UserState } from '../../state/user';
|
|||
import { loadUserSuccess } from '../../state/user/user.actions';
|
||||
import { selectUserState } from '../../state/user/user.selectors';
|
||||
|
||||
export const authGuard: CanMatchFn = (route, state) => {
|
||||
export const authenticationGuard: CanMatchFn = (route, state) => {
|
||||
const authStorage: AuthStorage = inject(AuthStorage);
|
||||
const authService: AuthService = inject(AuthService);
|
||||
const userService: UserService = inject(UserService);
|
|
@ -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 { CanMatchFn, Route, Router, UrlSegment } from '@angular/router';
|
||||
import { inject } from '@angular/core';
|
||||
import { map } from 'rxjs';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { User, UserState } from '../../state/user';
|
||||
import { selectUser } from '../../state/user/user.selectors';
|
||||
|
||||
export const authorizationGuard = (authorizationCheck: (user: User) => boolean): CanMatchFn => {
|
||||
return (route: Route, state: UrlSegment[]) => {
|
||||
const router: Router = inject(Router);
|
||||
const store: Store<UserState> = inject(Store<UserState>);
|
||||
|
||||
return store.select(selectUser).pipe(
|
||||
map((currentUser) => {
|
||||
if (authorizationCheck(currentUser)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO - replace with 404 error page
|
||||
return router.parseUrl('/');
|
||||
})
|
||||
);
|
||||
};
|
||||
};
|
|
@ -160,6 +160,30 @@ export class NiFiCommon {
|
|||
return groupString + bundle.artifact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two strings.
|
||||
*
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
public compareString(a: string, b: string): number {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return a < b ? -1 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two numbers.
|
||||
*
|
||||
* @param a
|
||||
* @param b
|
||||
*/
|
||||
public compareNumber(a: number, b: number): number {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant regex for leading and/or trailing whitespace.
|
||||
*/
|
||||
|
|
|
@ -59,18 +59,27 @@ export class ExtensionTypesEffects {
|
|||
combineLatest([
|
||||
this.extensionTypesService.getControllerServiceTypes(),
|
||||
this.extensionTypesService.getReportingTaskTypes(),
|
||||
this.extensionTypesService.getRegistryClientTypes(),
|
||||
this.extensionTypesService.getParameterProviderTypes(),
|
||||
this.extensionTypesService.getFlowAnalysisRuleTypes()
|
||||
]).pipe(
|
||||
map(([controllerServiceTypes, reportingTaskTypes, parameterProviderTypes, flowAnalysisRuleTypes]) =>
|
||||
ExtensionTypesActions.loadExtensionTypesForSettingsSuccess({
|
||||
response: {
|
||||
controllerServiceTypes: controllerServiceTypes.controllerServiceTypes,
|
||||
reportingTaskTypes: reportingTaskTypes.reportingTaskTypes,
|
||||
parameterProviderTypes: parameterProviderTypes.parameterProviderTypes,
|
||||
flowAnalysisRuleTypes: flowAnalysisRuleTypes.flowAnalysisRuleTypes
|
||||
}
|
||||
})
|
||||
map(
|
||||
([
|
||||
controllerServiceTypes,
|
||||
reportingTaskTypes,
|
||||
registryClientTypes,
|
||||
parameterProviderTypes,
|
||||
flowAnalysisRuleTypes
|
||||
]) =>
|
||||
ExtensionTypesActions.loadExtensionTypesForSettingsSuccess({
|
||||
response: {
|
||||
controllerServiceTypes: controllerServiceTypes.controllerServiceTypes,
|
||||
reportingTaskTypes: reportingTaskTypes.reportingTaskTypes,
|
||||
registryClientTypes: registryClientTypes.flowRegistryClientTypes,
|
||||
parameterProviderTypes: parameterProviderTypes.parameterProviderTypes,
|
||||
flowAnalysisRuleTypes: flowAnalysisRuleTypes.flowAnalysisRuleTypes
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) => of(ExtensionTypesActions.extensionTypesApiError({ error: error.error })))
|
||||
)
|
||||
|
|
|
@ -30,6 +30,7 @@ export const initialState: ExtensionTypesState = {
|
|||
controllerServiceTypes: [],
|
||||
prioritizerTypes: [],
|
||||
reportingTaskTypes: [],
|
||||
registryClientTypes: [],
|
||||
flowAnalysisRuleTypes: [],
|
||||
parameterProviderTypes: [],
|
||||
error: null,
|
||||
|
@ -54,6 +55,7 @@ export const extensionTypesReducer = createReducer(
|
|||
...state,
|
||||
controllerServiceTypes: response.controllerServiceTypes,
|
||||
reportingTaskTypes: response.reportingTaskTypes,
|
||||
registryClientTypes: response.registryClientTypes,
|
||||
parameterProviderTypes: response.parameterProviderTypes,
|
||||
flowAnalysisRuleTypes: response.flowAnalysisRuleTypes,
|
||||
error: null,
|
||||
|
|
|
@ -39,3 +39,8 @@ export const selectReportingTaskTypes = createSelector(
|
|||
selectExtensionTypesState,
|
||||
(state: ExtensionTypesState) => state.reportingTaskTypes
|
||||
);
|
||||
|
||||
export const selectRegistryClientTypes = createSelector(
|
||||
selectExtensionTypesState,
|
||||
(state: ExtensionTypesState) => state.registryClientTypes
|
||||
);
|
||||
|
|
|
@ -28,6 +28,7 @@ export interface LoadExtensionTypesForCanvasResponse {
|
|||
export interface LoadExtensionTypesForSettingsResponse {
|
||||
controllerServiceTypes: DocumentedType[];
|
||||
reportingTaskTypes: DocumentedType[];
|
||||
registryClientTypes: DocumentedType[];
|
||||
flowAnalysisRuleTypes: DocumentedType[];
|
||||
parameterProviderTypes: DocumentedType[];
|
||||
}
|
||||
|
@ -41,6 +42,7 @@ export interface ExtensionTypesState {
|
|||
controllerServiceTypes: DocumentedType[];
|
||||
prioritizerTypes: DocumentedType[];
|
||||
reportingTaskTypes: DocumentedType[];
|
||||
registryClientTypes: DocumentedType[];
|
||||
flowAnalysisRuleTypes: DocumentedType[];
|
||||
parameterProviderTypes: DocumentedType[];
|
||||
error: string | null;
|
||||
|
|
|
@ -32,5 +32,13 @@
|
|||
.mat-column-property {
|
||||
min-width: 230px;
|
||||
}
|
||||
|
||||
.mat-column-value {
|
||||
min-width: 230px;
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
min-width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue