mirror of https://github.com/apache/nifi.git
NIFI-12543: Users/User Groups (#8191)
* NIFI-12543: - Users/User Groups. * NIFI-12543: - Users/User Groups Deletion. - Establishing routes for selection, editing, and access policies. * NIFI-12543: - User/User Group Creation. - User/User Group Configuration. - Renaming existing User State to Current User State. * NIFI-12543: - User access policies table. * NIFI-12543: - Sorting users/groups in the edit dialog. * NIFI-12543: - Addressing review feedback. This closes #8191
This commit is contained in:
parent
aaa812b1b5
commit
76f880588f
|
@ -47,6 +47,11 @@ const routes: Routes = [
|
|||
canMatch: [authenticationGuard],
|
||||
loadChildren: () => import('./pages/counters/feature/counters.module').then((m) => m.CountersModule)
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
canMatch: [authenticationGuard],
|
||||
loadChildren: () => import('./pages/users/feature/users.module').then((m) => m.UsersModule)
|
||||
},
|
||||
{
|
||||
path: 'summary',
|
||||
canMatch: [authenticationGuard],
|
||||
|
|
|
@ -27,7 +27,7 @@ import { environment } from './environments/environment';
|
|||
import { HTTP_INTERCEPTORS, HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
|
||||
import { NavigationActionTiming, RouterState, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||
import { rootReducers } from './state';
|
||||
import { UserEffects } from './state/user/user.effects';
|
||||
import { CurrentUserEffects } from './state/current-user/current-user.effects';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { LoadingInterceptor } from './service/interceptors/loading.interceptor';
|
||||
import { AuthInterceptor } from './service/interceptors/auth.interceptor';
|
||||
|
@ -59,7 +59,7 @@ import { SystemDiagnosticsEffects } from './state/system-diagnostics/system-diag
|
|||
navigationActionTiming: NavigationActionTiming.PostActivation
|
||||
}),
|
||||
EffectsModule.forRoot(
|
||||
UserEffects,
|
||||
CurrentUserEffects,
|
||||
ExtensionTypesEffects,
|
||||
AboutEffects,
|
||||
StatusHistoryEffects,
|
||||
|
|
|
@ -19,13 +19,13 @@ 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';
|
||||
import { CurrentUser } from '../../../state/current-user';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Counters,
|
||||
canMatch: [authorizationGuard((user: User) => user.countersPermissions.canRead)]
|
||||
canMatch: [authorizationGuard((user: CurrentUser) => user.countersPermissions.canRead)]
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../state';
|
||||
import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
|
||||
import { startCurrentUserPolling, stopCurrentUserPolling } from '../../../state/current-user/current-user.actions';
|
||||
import { resetCounterState } from '../state/counter-listing/counter-listing.actions';
|
||||
|
||||
@Component({
|
||||
|
@ -30,11 +30,11 @@ export class Counters implements OnInit, OnDestroy {
|
|||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(startUserPolling());
|
||||
this.store.dispatch(startCurrentUserPolling());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(resetCounterState());
|
||||
this.store.dispatch(stopUserPolling());
|
||||
this.store.dispatch(stopCurrentUserPolling());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import { countersFeatureKey, reducers } from '../state';
|
|||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { CounterListingEffects } from '../state/counter-listing/counter-listing.effects';
|
||||
import { CounterListingModule } from '../ui/counter-listing/counter-listing.module';
|
||||
import { ParameterContextListingModule } from '../../parameter-contexts/ui/parameter-context-listing/parameter-context-listing.module';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
|
||||
@NgModule({
|
||||
|
@ -36,7 +35,6 @@ import { MatDialogModule } from '@angular/material/dialog';
|
|||
StoreModule.forFeature(countersFeatureKey, reducers),
|
||||
EffectsModule.forFeature(CounterListingEffects),
|
||||
CounterListingModule,
|
||||
ParameterContextListingModule,
|
||||
MatDialogModule
|
||||
]
|
||||
})
|
||||
|
|
|
@ -17,8 +17,13 @@
|
|||
|
||||
import { CounterListingState } from './index';
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import { loadCounters, loadCountersSuccess, resetCounterState, resetCounterSuccess } from './counter-listing.actions';
|
||||
import { parameterContextListingApiError } from '../../../parameter-contexts/state/parameter-context-listing/parameter-context-listing.actions';
|
||||
import {
|
||||
counterListingApiError,
|
||||
loadCounters,
|
||||
loadCountersSuccess,
|
||||
resetCounterState,
|
||||
resetCounterSuccess
|
||||
} from './counter-listing.actions';
|
||||
import { produce } from 'immer';
|
||||
|
||||
export const initialState: CounterListingState = {
|
||||
|
@ -42,7 +47,7 @@ export const counterListingReducer = createReducer(
|
|||
error: null,
|
||||
status: 'success' as const
|
||||
})),
|
||||
on(parameterContextListingApiError, (state, { error }) => ({
|
||||
on(counterListingApiError, (state, { error }) => ({
|
||||
...state,
|
||||
saving: false,
|
||||
error,
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Store } from '@ngrx/store';
|
|||
import { loadCounters, promptCounterReset } from '../../state/counter-listing/counter-listing.actions';
|
||||
import { selectCounterListingState } from '../../state/counter-listing/counter-listing.selectors';
|
||||
import { initialState } from '../../state/counter-listing/counter-listing.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
|
||||
@Component({
|
||||
selector: 'counter-listing',
|
||||
|
@ -30,7 +30,7 @@ import { selectUser } from '../../../../state/user/user.selectors';
|
|||
})
|
||||
export class CounterListing implements OnInit {
|
||||
counterListingState$ = this.store.select(selectCounterListingState);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
|
||||
constructor(private store: Store<CounterListingState>) {}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
|
||||
import { startCurrentUserPolling, stopCurrentUserPolling } from '../../../state/current-user/current-user.actions';
|
||||
import { loadExtensionTypesForCanvas } from '../../../state/extension-types/extension-types.actions';
|
||||
import { NiFiState } from '../../../state';
|
||||
|
||||
|
@ -30,11 +30,11 @@ export class FlowDesigner implements OnInit, OnDestroy {
|
|||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(startUserPolling());
|
||||
this.store.dispatch(startCurrentUserPolling());
|
||||
this.store.dispatch(loadExtensionTypesForCanvas());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(stopUserPolling());
|
||||
this.store.dispatch(stopCurrentUserPolling());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ import { CanvasState } from '../../state';
|
|||
import { transformFeatureKey } from '../../state/transform';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -53,7 +53,7 @@ describe('ConnectableBehavior', () => {
|
|||
value: initialState[flowFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,8 +28,8 @@ import { transformFeatureKey } from '../../state/transform';
|
|||
import { selectFlowState } from '../../state/flow/flow.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('DraggableBehavior', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,8 +28,8 @@ import * as fromTransform from '../../state/transform/transform.reducer';
|
|||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('EditableBehaviorService', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -27,8 +27,8 @@ import { transformFeatureKey } from '../../state/transform';
|
|||
import * as fromTransform from '../../state/transform/transform.reducer';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -53,7 +53,7 @@ describe('QuickSelectBehavior', () => {
|
|||
value: initialState[flowFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -26,8 +26,8 @@ import { provideMockStore } from '@ngrx/store/testing';
|
|||
import { selectFlowState } from '../../state/flow/flow.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -52,7 +52,7 @@ describe('SelectableBehavior', () => {
|
|||
value: initialState[flowFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,8 +28,8 @@ import { selectFlowState } from '../state/flow/flow.selectors';
|
|||
import { selectTransform } from '../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../state/controller-services';
|
||||
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../state/parameter';
|
||||
import * as fromParameter from '../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('BirdseyeView', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -27,8 +27,8 @@ import { provideMockStore } from '@ngrx/store/testing';
|
|||
import { selectFlowState } from '../state/flow/flow.selectors';
|
||||
import { controllerServicesFeatureKey } from '../state/controller-services';
|
||||
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../state/parameter';
|
||||
import * as fromParameter from '../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -53,7 +53,7 @@ describe('CanvasUtils', () => {
|
|||
value: initialState[flowFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -32,9 +32,9 @@ import { BulletinsTip } from '../../../ui/common/tooltips/bulletins-tip/bulletin
|
|||
import { Position } from '../state/shared';
|
||||
import { ComponentType, Permissions } from '../../../state/shared';
|
||||
import { NiFiCommon } from '../../../service/nifi-common.service';
|
||||
import { User } from '../../../state/user';
|
||||
import { initialState as initialUserState } from '../../../state/user/user.reducer';
|
||||
import { selectUser } from '../../../state/user/user.selectors';
|
||||
import { CurrentUser } from '../../../state/current-user';
|
||||
import { initialState as initialUserState } from '../../../state/current-user/current-user.reducer';
|
||||
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -48,7 +48,7 @@ export class CanvasUtils {
|
|||
private currentProcessGroupId: string = initialFlowState.id;
|
||||
private parentProcessGroupId: string | null = initialFlowState.flow.processGroupFlow.parentGroupId;
|
||||
private canvasPermissions: Permissions = initialFlowState.flow.permissions;
|
||||
private currentUser: User = initialUserState.user;
|
||||
private currentUser: CurrentUser = initialUserState.user;
|
||||
private connections: any[] = [];
|
||||
|
||||
private readonly humanizeDuration: Humanizer;
|
||||
|
@ -89,7 +89,7 @@ export class CanvasUtils {
|
|||
});
|
||||
|
||||
this.store
|
||||
.select(selectUser)
|
||||
.select(selectCurrentUser)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((user) => {
|
||||
this.currentUser = user;
|
||||
|
|
|
@ -28,8 +28,8 @@ import { selectFlowState } from '../state/flow/flow.selectors';
|
|||
import { selectTransform } from '../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../state/controller-services';
|
||||
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../state/parameter';
|
||||
import * as fromParameter from '../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('CanvasView', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,8 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
|
|||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('ConnectionManager', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,8 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
|
|||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('FunnelManager', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,8 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
|
|||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('LabelManager', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,8 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
|
|||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('PortManager', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,8 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
|
|||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('ProcessGroupManager', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,8 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
|
|||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('ProcessorManager', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -28,8 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
|
|||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../state/current-user/current-user.reducer';
|
||||
import { parameterFeatureKey } from '../../state/parameter';
|
||||
import * as fromParameter from '../../state/parameter/parameter.reducer';
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe('RemoteProcessGroupManager', () => {
|
|||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -115,7 +115,11 @@
|
|||
Node Status History
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item class="global-menu-item">
|
||||
<button
|
||||
mat-menu-item
|
||||
class="global-menu-item"
|
||||
[routerLink]="['/users']"
|
||||
[disabled]="!user.tenantsPermissions.canRead">
|
||||
<i class="fa fa-fw fa-users mr-2"></i>
|
||||
Users
|
||||
</button>
|
||||
|
|
|
@ -36,8 +36,8 @@ import { ClusterSummary, ControllerStatus } from '../../../state/flow';
|
|||
import { Search } from './search/search.component';
|
||||
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { selectUser } from '../../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../../state/user/user.reducer';
|
||||
import { selectCurrentUser } from '../../../../../state/current-user/current-user.selectors';
|
||||
import * as fromUser from '../../../../../state/current-user/current-user.reducer';
|
||||
|
||||
describe('HeaderComponent', () => {
|
||||
let component: HeaderComponent;
|
||||
|
@ -103,7 +103,7 @@ describe('HeaderComponent', () => {
|
|||
value: []
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
selector: selectCurrentUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
|
|
|
@ -26,8 +26,8 @@ import {
|
|||
selectCurrentProcessGroupId,
|
||||
selectLastRefreshed
|
||||
} from '../../../state/flow/flow.selectors';
|
||||
import { selectUser } from '../../../../../state/user/user.selectors';
|
||||
import { User } from '../../../../../state/user';
|
||||
import { selectCurrentUser } from '../../../../../state/current-user/current-user.selectors';
|
||||
import { CurrentUser } from '../../../../../state/current-user';
|
||||
import { AuthStorage } from '../../../../../service/auth-storage.service';
|
||||
import { AuthService } from '../../../../../service/auth.service';
|
||||
import { LoadingService } from '../../../../../service/loading.service';
|
||||
|
@ -63,7 +63,7 @@ export class HeaderComponent {
|
|||
lastRefreshed$ = this.store.select(selectLastRefreshed);
|
||||
clusterSummary$ = this.store.select(selectClusterSummary);
|
||||
controllerBulletins$ = this.store.select(selectControllerBulletins);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
currentProcessGroupId$ = this.store.select(selectCurrentProcessGroupId);
|
||||
|
||||
constructor(
|
||||
|
@ -73,7 +73,7 @@ export class HeaderComponent {
|
|||
public loadingService: LoadingService
|
||||
) {}
|
||||
|
||||
allowLogin(user: User): boolean {
|
||||
allowLogin(user: CurrentUser): boolean {
|
||||
return user.anonymous && location.protocol === 'https:';
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { FlowService } from '../../../service/flow.service';
|
|||
import { inject } from '@angular/core';
|
||||
import { switchMap, take } from 'rxjs';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { UserState } from '../../../../../state/user';
|
||||
import { CurrentUserState } from '../../../../../state/current-user';
|
||||
import { FlowState } from '../../../state/flow';
|
||||
import { selectCurrentProcessGroupId } from '../../../state/flow/flow.selectors';
|
||||
import { initialState } from '../../../state/flow/flow.reducer';
|
||||
|
@ -28,7 +28,7 @@ import { initialState } from '../../../state/flow/flow.reducer';
|
|||
export const rootGroupGuard: CanActivateFn = (route, state) => {
|
||||
const router: Router = inject(Router);
|
||||
const flowService: FlowService = inject(FlowService);
|
||||
const store: Store<UserState> = inject(Store<FlowState>);
|
||||
const store: Store<CurrentUserState> = inject(Store<FlowState>);
|
||||
|
||||
return store.select(selectCurrentProcessGroupId).pipe(
|
||||
take(1),
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||
|
||||
import { Login } from './login.component';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../state/user/user.reducer';
|
||||
import { initialState } from '../../../state/current-user/current-user.reducer';
|
||||
|
||||
describe('Login', () => {
|
||||
let component: Login;
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||
|
||||
import { LoginForm } from './login-form.component';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../../state/user/user.reducer';
|
||||
import { initialState } from '../../../../state/current-user/current-user.reducer';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../state';
|
||||
import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
|
||||
import { startCurrentUserPolling, stopCurrentUserPolling } from '../../../state/current-user/current-user.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'parameter-contexts',
|
||||
|
@ -29,10 +29,10 @@ export class ParameterContexts implements OnInit, OnDestroy {
|
|||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(startUserPolling());
|
||||
this.store.dispatch(startCurrentUserPolling());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(stopUserPolling());
|
||||
this.store.dispatch(stopCurrentUserPolling());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../state';
|
||||
import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
|
||||
import { startCurrentUserPolling, stopCurrentUserPolling } from '../../../state/current-user/current-user.actions';
|
||||
import { loadProvenanceOptions } from '../state/provenance-event-listing/provenance-event-listing.actions';
|
||||
import { loadAbout } from '../../../state/about/about.actions';
|
||||
|
||||
|
@ -31,12 +31,12 @@ export class Provenance implements OnInit, OnDestroy {
|
|||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(startUserPolling());
|
||||
this.store.dispatch(startCurrentUserPolling());
|
||||
this.store.dispatch(loadProvenanceOptions());
|
||||
this.store.dispatch(loadAbout());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(stopUserPolling());
|
||||
this.store.dispatch(stopCurrentUserPolling());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,13 +25,13 @@ import { FlowAnalysisRules } from '../ui/flow-analysis-rules/flow-analysis-rules
|
|||
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';
|
||||
import { CurrentUser } from '../../../state/current-user';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Settings,
|
||||
canMatch: [authorizationGuard((user: User) => user.controllerPermissions.canRead)],
|
||||
canMatch: [authorizationGuard((user: CurrentUser) => user.controllerPermissions.canRead)],
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'general' },
|
||||
{ path: 'general', component: General },
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../state';
|
||||
import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
|
||||
import { startCurrentUserPolling, stopCurrentUserPolling } from '../../../state/current-user/current-user.actions';
|
||||
import { loadExtensionTypesForSettings } from '../../../state/extension-types/extension-types.actions';
|
||||
|
||||
@Component({
|
||||
|
@ -57,11 +57,11 @@ export class Settings implements OnInit, OnDestroy {
|
|||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(startUserPolling());
|
||||
this.store.dispatch(startCurrentUserPolling());
|
||||
this.store.dispatch(loadExtensionTypesForSettings());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(stopUserPolling());
|
||||
this.store.dispatch(stopCurrentUserPolling());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +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';
|
||||
import { selectCurrentUser } from '../../../../../state/current-user/current-user.selectors';
|
||||
|
||||
@Component({
|
||||
selector: 'general-form',
|
||||
|
@ -36,7 +36,7 @@ export class GeneralForm {
|
|||
this.controllerForm.get('timerDrivenThreadCount')?.setValue(controller.component.maxTimerDrivenThreadCount);
|
||||
}
|
||||
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
controllerForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
|
|
|
@ -39,7 +39,7 @@ 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 { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { state } from '@angular/animations';
|
||||
import { resetEnableControllerServiceState } from '../../../../state/contoller-service-state/controller-service-state.actions';
|
||||
|
@ -52,7 +52,7 @@ import { resetEnableControllerServiceState } from '../../../../state/contoller-s
|
|||
export class ManagementControllerServices implements OnInit, OnDestroy {
|
||||
serviceState$ = this.store.select(selectManagementControllerServicesState);
|
||||
selectedServiceId$ = this.store.select(selectControllerServiceIdFromRoute);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
|
||||
constructor(private store: Store<NiFiState>) {
|
||||
this.store
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
} 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 { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { filter, switchMap, take } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
@ -48,7 +48,7 @@ import { state } from '@angular/animations';
|
|||
export class RegistryClients implements OnInit, OnDestroy {
|
||||
registryClientsState$ = this.store.select(selectRegistryClientsState);
|
||||
selectedRegistryClientId$ = this.store.select(selectRegistryClientIdFromRoute);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
|
||||
constructor(private store: Store<NiFiState>) {
|
||||
this.store
|
||||
|
|
|
@ -32,7 +32,7 @@ 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 { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { state } from '@angular/animations';
|
||||
|
||||
|
@ -44,7 +44,7 @@ import { state } from '@angular/animations';
|
|||
export class ReportingTasks implements OnInit, OnDestroy {
|
||||
reportingTaskState$ = this.store.select(selectReportingTasksState);
|
||||
selectedReportingTaskId$ = this.store.select(selectReportingTaskIdFromRoute);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
|
||||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../state';
|
||||
import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
|
||||
import { startCurrentUserPolling, stopCurrentUserPolling } from '../../../state/current-user/current-user.actions';
|
||||
import { loadSummaryListing, resetSummaryState } from '../state/summary-listing/summary-listing.actions';
|
||||
|
||||
interface TabLink {
|
||||
|
@ -44,12 +44,12 @@ export class Summary implements OnInit, OnDestroy {
|
|||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(startUserPolling());
|
||||
this.store.dispatch(startCurrentUserPolling());
|
||||
this.store.dispatch(loadSummaryListing({ recursive: true }));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(resetSummaryState());
|
||||
this.store.dispatch(stopUserPolling());
|
||||
this.store.dispatch(stopCurrentUserPolling());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ import {
|
|||
selectSummaryListingStatus,
|
||||
selectViewStatusHistory
|
||||
} from '../../state/summary-listing/summary-listing.selectors';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import { filter, switchMap, take } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import {
|
||||
|
@ -51,7 +51,7 @@ import { getSystemDiagnosticsAndOpenDialog } from '../../../../state/system-diag
|
|||
export class ConnectionStatusListing {
|
||||
loadedTimestamp$ = this.store.select(selectSummaryListingLoadedTimestamp);
|
||||
summaryListingStatus$ = this.store.select(selectSummaryListingStatus);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
connectionStatusSnapshots$ = this.store.select(selectConnectionStatusSnapshots);
|
||||
selectedConnectionId$ = this.store.select(selectConnectionIdFromRoute);
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
selectSummaryListingLoadedTimestamp,
|
||||
selectSummaryListingStatus
|
||||
} from '../../state/summary-listing/summary-listing.selectors';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import { PortStatusSnapshotEntity, SummaryListingState } from '../../state/summary-listing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { initialState } from '../../state/summary-listing/summary-listing.reducer';
|
||||
|
@ -38,7 +38,7 @@ export class InputPortStatusListing {
|
|||
portStatusSnapshots$ = this.store.select(selectInputPortStatusSnapshots);
|
||||
loadedTimestamp$ = this.store.select(selectSummaryListingLoadedTimestamp);
|
||||
summaryListingStatus$ = this.store.select(selectSummaryListingStatus);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
selectedPortId$ = this.store.select(selectInputPortIdFromRoute);
|
||||
|
||||
constructor(private store: Store<SummaryListingState>) {}
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
selectSummaryListingLoadedTimestamp,
|
||||
selectSummaryListingStatus
|
||||
} from '../../state/summary-listing/summary-listing.selectors';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { PortStatusSnapshotEntity, SummaryListingState } from '../../state/summary-listing';
|
||||
import { initialState } from '../../state/summary-listing/summary-listing.reducer';
|
||||
|
@ -40,7 +40,7 @@ export class OutputPortStatusListing {
|
|||
portStatusSnapshots$ = this.store.select(selectOutputPortStatusSnapshots);
|
||||
loadedTimestamp$ = this.store.select(selectSummaryListingLoadedTimestamp);
|
||||
summaryListingStatus$ = this.store.select(selectSummaryListingStatus);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
selectedPortId$ = this.store.select(selectOutputPortIdFromRoute);
|
||||
|
||||
constructor(private store: Store<SummaryListingState>) {}
|
||||
|
|
|
@ -42,7 +42,7 @@ import {
|
|||
openStatusHistoryDialog
|
||||
} from '../../../../state/status-history/status-history.actions';
|
||||
import { ComponentType } from '../../../../state/shared';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import { getSystemDiagnosticsAndOpenDialog } from '../../../../state/system-diagnostics/system-diagnostics.actions';
|
||||
|
||||
@Component({
|
||||
|
@ -54,7 +54,7 @@ export class ProcessGroupStatusListing {
|
|||
processGroupStatusSnapshots$ = this.store.select(selectProcessGroupStatusSnapshots);
|
||||
loadedTimestamp$ = this.store.select(selectSummaryListingLoadedTimestamp);
|
||||
summaryListingStatus$ = this.store.select(selectSummaryListingStatus);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
selectedProcessGroupId$ = this.store.select(selectProcessGroupIdFromRoute);
|
||||
processGroupStatus$ = this.store.select(selectProcessGroupStatus);
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
selectViewStatusHistory
|
||||
} from '../../state/summary-listing/summary-listing.selectors';
|
||||
import { ProcessorStatusSnapshotEntity, SummaryListingState } from '../../state/summary-listing';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import { initialState } from '../../state/summary-listing/summary-listing.reducer';
|
||||
import {
|
||||
getStatusHistoryAndOpenDialog,
|
||||
|
@ -49,7 +49,7 @@ export class ProcessorStatusListing {
|
|||
summaryListingStatus$ = this.store.select(selectSummaryListingStatus);
|
||||
selectedProcessorId$ = this.store.select(selectProcessorIdFromRoute);
|
||||
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
|
||||
constructor(private store: Store<SummaryListingState>) {
|
||||
this.store
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
selectSummaryListingStatus,
|
||||
selectViewStatusHistory
|
||||
} from '../../state/summary-listing/summary-listing.selectors';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { RemoteProcessGroupStatusSnapshotEntity, SummaryListingState } from '../../state/summary-listing';
|
||||
import { filter, switchMap, take } from 'rxjs';
|
||||
|
@ -46,7 +46,7 @@ import { getSystemDiagnosticsAndOpenDialog } from '../../../../state/system-diag
|
|||
export class RemoteProcessGroupStatusListing {
|
||||
loadedTimestamp$ = this.store.select(selectSummaryListingLoadedTimestamp);
|
||||
summaryListingStatus$ = this.store.select(selectSummaryListingStatus);
|
||||
currentUser$ = this.store.select(selectUser);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
rpgStatusSnapshots$ = this.store.select(selectRemoteProcessGroupStatusSnapshots);
|
||||
selectedRpgId$ = this.store.select(selectRemoteProcessGroupIdFromRoute);
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { RouterModule, Routes } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Users } from './users.component';
|
||||
import { authorizationGuard } from '../../../service/guard/authorization.guard';
|
||||
import { CurrentUser } from '../../../state/current-user';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Users,
|
||||
canMatch: [authorizationGuard((user: CurrentUser) => user.tenantsPermissions.canRead)],
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: Users,
|
||||
children: [
|
||||
{
|
||||
path: 'edit',
|
||||
component: Users
|
||||
},
|
||||
{
|
||||
path: 'policies',
|
||||
component: Users
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class UsersRoutingModule {}
|
|
@ -0,0 +1,28 @@
|
|||
<!--
|
||||
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
~ contributor license agreements. See the NOTICE file distributed with
|
||||
~ this work for additional information regarding copyright ownership.
|
||||
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
~ (the "License"); you may not use this file except in compliance with
|
||||
~ the License. You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
|
||||
<div class="flex justify-between">
|
||||
<h3 class="text-xl bold user-header">NiFi Users</h3>
|
||||
<button class="nifi-button" [routerLink]="['/']">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<user-listing></user-listing>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,20 @@
|
|||
/*!
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.user-header {
|
||||
color: #728e9b;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { Users } from './users.component';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { UserListing } from '../ui/user-listing/user-listing.component';
|
||||
import { initialState } from '../state/user-listing/user-listing.reducer';
|
||||
|
||||
describe('Users', () => {
|
||||
let component: Users;
|
||||
let fixture: ComponentFixture<Users>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [Users, UserListing],
|
||||
imports: [RouterModule, RouterTestingModule],
|
||||
providers: [provideMockStore({ initialState })]
|
||||
});
|
||||
fixture = TestBed.createComponent(Users);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../state';
|
||||
import { startCurrentUserPolling, stopCurrentUserPolling } from '../../../state/current-user/current-user.actions';
|
||||
import { resetUsersState } from '../state/user-listing/user-listing.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'users',
|
||||
templateUrl: './users.component.html',
|
||||
styleUrls: ['./users.component.scss']
|
||||
})
|
||||
export class Users implements OnInit, OnDestroy {
|
||||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(startCurrentUserPolling());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(resetUsersState());
|
||||
this.store.dispatch(stopCurrentUserPolling());
|
||||
}
|
||||
}
|
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Users } from './users.component';
|
||||
import { UsersRoutingModule } from './users-routing.module';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { usersFeatureKey, reducers } from '../state';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { UserListingEffects } from '../state/user-listing/user-listing.effects';
|
||||
import { UserListingModule } from '../ui/user-listing/user-listing.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [Users],
|
||||
exports: [Users],
|
||||
imports: [
|
||||
CommonModule,
|
||||
UsersRoutingModule,
|
||||
StoreModule.forFeature(usersFeatureKey, reducers),
|
||||
EffectsModule.forFeature(UserListingEffects),
|
||||
MatDialogModule,
|
||||
UserListingModule
|
||||
]
|
||||
})
|
||||
export class UsersModule {}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { HttpClient } from '@angular/common/http';
|
||||
import { Client } from '../../../service/client.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NiFiCommon } from '../../../service/nifi-common.service';
|
||||
import { UserEntity, UserGroupEntity } from '../../../state/shared';
|
||||
import {
|
||||
CreateUserGroupRequest,
|
||||
CreateUserRequest,
|
||||
UpdateUserGroupRequest,
|
||||
UpdateUserRequest
|
||||
} from '../state/user-listing';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UsersService {
|
||||
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
|
||||
) {}
|
||||
|
||||
getUsers(): Observable<any> {
|
||||
return this.httpClient.get(`${UsersService.API}/tenants/users`);
|
||||
}
|
||||
|
||||
getUserGroups(): Observable<any> {
|
||||
return this.httpClient.get(`${UsersService.API}/tenants/user-groups`);
|
||||
}
|
||||
|
||||
createUser(request: CreateUserRequest): Observable<any> {
|
||||
const payload: any = {
|
||||
revision: request.revision,
|
||||
component: request.userPayload
|
||||
};
|
||||
return this.httpClient.post(`${UsersService.API}/tenants/users`, payload);
|
||||
}
|
||||
|
||||
createUserGroup(request: CreateUserGroupRequest): Observable<any> {
|
||||
const payload: any = {
|
||||
revision: request.revision,
|
||||
component: request.userGroupPayload
|
||||
};
|
||||
return this.httpClient.post(`${UsersService.API}/tenants/user-groups`, payload);
|
||||
}
|
||||
|
||||
updateUser(request: UpdateUserRequest): Observable<any> {
|
||||
const payload: any = {
|
||||
revision: request.revision,
|
||||
component: {
|
||||
id: request.id,
|
||||
...request.userPayload
|
||||
}
|
||||
};
|
||||
return this.httpClient.put(this.stripProtocol(request.uri), payload);
|
||||
}
|
||||
|
||||
updateUserGroup(request: UpdateUserGroupRequest): Observable<any> {
|
||||
const payload: any = {
|
||||
revision: request.revision,
|
||||
component: {
|
||||
id: request.id,
|
||||
...request.userGroupPayload
|
||||
}
|
||||
};
|
||||
return this.httpClient.put(this.stripProtocol(request.uri), payload);
|
||||
}
|
||||
|
||||
deleteUser(user: UserEntity): Observable<any> {
|
||||
const revision: any = this.client.getRevision(user);
|
||||
return this.httpClient.delete(this.stripProtocol(user.uri), { params: revision });
|
||||
}
|
||||
|
||||
deleteUserGroup(userGroup: UserGroupEntity): Observable<any> {
|
||||
const revision: any = this.client.getRevision(userGroup);
|
||||
return this.httpClient.delete(this.stripProtocol(userGroup.uri), { params: revision });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
|
||||
import { userListingReducer } from './user-listing/user-listing.reducer';
|
||||
import { UserListingState } from './user-listing';
|
||||
|
||||
export const usersFeatureKey = 'users';
|
||||
|
||||
export interface UsersState {
|
||||
[usersFeatureKey]: UserListingState;
|
||||
}
|
||||
|
||||
export function reducers(state: UsersState | undefined, action: Action) {
|
||||
return combineReducers({
|
||||
[usersFeatureKey]: userListingReducer
|
||||
})(state, action);
|
||||
}
|
||||
|
||||
export const selectUserState = createFeatureSelector<UsersState>(usersFeatureKey);
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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 { AccessPolicySummaryEntity, Revision, UserEntity, UserGroupEntity } from '../../../../state/shared';
|
||||
|
||||
export interface SelectedTenant {
|
||||
id: string;
|
||||
user?: UserEntity;
|
||||
userGroup?: UserGroupEntity;
|
||||
}
|
||||
|
||||
export interface LoadTenantsSuccess {
|
||||
users: UserEntity[];
|
||||
userGroups: UserGroupEntity[];
|
||||
loadedTimestamp: string;
|
||||
}
|
||||
|
||||
export interface CreateUserRequest {
|
||||
revision: Revision;
|
||||
userPayload: any;
|
||||
userGroupUpdate?: {
|
||||
requestId: number;
|
||||
userGroups: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateUserResponse {
|
||||
user: UserEntity;
|
||||
userGroupUpdate?: {
|
||||
requestId: number;
|
||||
userGroups: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateUserGroupRequest {
|
||||
revision: Revision;
|
||||
userGroupPayload: any;
|
||||
}
|
||||
|
||||
export interface CreateUserGroupResponse {
|
||||
userGroup: UserGroupEntity;
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
revision: Revision;
|
||||
id: string;
|
||||
uri: string;
|
||||
userPayload: any;
|
||||
userGroupUpdate?: {
|
||||
requestId: number;
|
||||
userGroupsAdded: string[];
|
||||
userGroupsRemoved: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateUserResponse {
|
||||
user: UserEntity;
|
||||
userGroupUpdate?: {
|
||||
requestId: number;
|
||||
userGroupsAdded: string[];
|
||||
userGroupsRemoved: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateUserGroupRequest {
|
||||
requestId?: number;
|
||||
revision: Revision;
|
||||
id: string;
|
||||
uri: string;
|
||||
userGroupPayload: any;
|
||||
}
|
||||
|
||||
export interface UpdateUserGroupResponse {
|
||||
requestId?: number;
|
||||
userGroup: UserGroupEntity;
|
||||
}
|
||||
|
||||
export interface EditUserDialogRequest {
|
||||
user: UserEntity;
|
||||
}
|
||||
|
||||
export interface EditUserGroupDialogRequest {
|
||||
userGroup: UserGroupEntity;
|
||||
}
|
||||
|
||||
export interface DeleteUserRequest {
|
||||
user: UserEntity;
|
||||
}
|
||||
|
||||
export interface DeleteUserGroupRequest {
|
||||
userGroup: UserGroupEntity;
|
||||
}
|
||||
|
||||
export interface UserAccessPoliciesDialogRequest {
|
||||
id: string;
|
||||
identity: string;
|
||||
accessPolicies: AccessPolicySummaryEntity[];
|
||||
}
|
||||
|
||||
export interface UserListingState {
|
||||
users: UserEntity[];
|
||||
userGroups: UserGroupEntity[];
|
||||
saving: boolean;
|
||||
loadedTimestamp: string;
|
||||
error: string | null;
|
||||
status: 'pending' | 'loading' | 'error' | 'success';
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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 {
|
||||
CreateUserGroupRequest,
|
||||
CreateUserGroupResponse,
|
||||
CreateUserRequest,
|
||||
CreateUserResponse,
|
||||
DeleteUserGroupRequest,
|
||||
DeleteUserRequest,
|
||||
EditUserGroupDialogRequest,
|
||||
EditUserDialogRequest,
|
||||
LoadTenantsSuccess,
|
||||
UserAccessPoliciesDialogRequest,
|
||||
UpdateUserRequest,
|
||||
UpdateUserResponse,
|
||||
UpdateUserGroupRequest,
|
||||
UpdateUserGroupResponse
|
||||
} from './index';
|
||||
|
||||
const USER_PREFIX: string = '[User Listing]';
|
||||
|
||||
export const resetUsersState = createAction(`${USER_PREFIX} Reset Users State`);
|
||||
|
||||
export const loadTenants = createAction(`${USER_PREFIX} Load Tenants`);
|
||||
|
||||
export const loadTenantsSuccess = createAction(
|
||||
`${USER_PREFIX} Load Tenants Success`,
|
||||
props<{ response: LoadTenantsSuccess }>()
|
||||
);
|
||||
|
||||
export const usersApiError = createAction(`${USER_PREFIX} Users Api Error`, props<{ error: string }>());
|
||||
|
||||
export const openCreateTenantDialog = createAction(`${USER_PREFIX} Open Create Tenant Dialog`);
|
||||
|
||||
export const createUser = createAction(`${USER_PREFIX} Create User`, props<{ request: CreateUserRequest }>());
|
||||
|
||||
export const createUserSuccess = createAction(
|
||||
`${USER_PREFIX} Create User Success`,
|
||||
props<{
|
||||
response: CreateUserResponse;
|
||||
}>()
|
||||
);
|
||||
|
||||
export const createUserComplete = createAction(
|
||||
`${USER_PREFIX} Create User Complete`,
|
||||
props<{
|
||||
response: CreateUserResponse;
|
||||
}>()
|
||||
);
|
||||
|
||||
export const createUserGroup = createAction(
|
||||
`${USER_PREFIX} Create User Group`,
|
||||
props<{
|
||||
request: CreateUserGroupRequest;
|
||||
}>()
|
||||
);
|
||||
|
||||
export const createUserGroupSuccess = createAction(
|
||||
`${USER_PREFIX} Create User Group Success`,
|
||||
props<{
|
||||
response: CreateUserGroupResponse;
|
||||
}>()
|
||||
);
|
||||
|
||||
export const updateUser = createAction(`${USER_PREFIX} Update User`, props<{ request: UpdateUserRequest }>());
|
||||
|
||||
export const updateUserSuccess = createAction(
|
||||
`${USER_PREFIX} Update User Success`,
|
||||
props<{
|
||||
response: UpdateUserResponse;
|
||||
}>()
|
||||
);
|
||||
|
||||
export const updateUserComplete = createAction(`${USER_PREFIX} Update User Complete`);
|
||||
|
||||
export const updateUserGroup = createAction(
|
||||
`${USER_PREFIX} Update User Group`,
|
||||
props<{
|
||||
request: UpdateUserGroupRequest;
|
||||
}>()
|
||||
);
|
||||
|
||||
export const updateUserGroupSuccess = createAction(
|
||||
`${USER_PREFIX} Update User Group Success`,
|
||||
props<{
|
||||
response: UpdateUserGroupResponse;
|
||||
}>()
|
||||
);
|
||||
|
||||
export const selectTenant = createAction(`${USER_PREFIX} Select Tenant`, props<{ id: string }>());
|
||||
|
||||
export const navigateToEditTenant = createAction(`${USER_PREFIX} Navigate To Edit Tenant`, props<{ id: string }>());
|
||||
|
||||
export const openConfigureUserDialog = createAction(
|
||||
`${USER_PREFIX} Open Configure User Dialog`,
|
||||
props<{ request: EditUserDialogRequest }>()
|
||||
);
|
||||
|
||||
export const openConfigureUserGroupDialog = createAction(
|
||||
`${USER_PREFIX} Open Configure User Group Dialog`,
|
||||
props<{ request: EditUserGroupDialogRequest }>()
|
||||
);
|
||||
|
||||
export const navigateToViewAccessPolicies = createAction(
|
||||
`${USER_PREFIX} Navigate To View Access Policies`,
|
||||
props<{ id: string }>()
|
||||
);
|
||||
|
||||
export const openUserAccessPoliciesDialog = createAction(
|
||||
`${USER_PREFIX} Open User Access Policy Dialog`,
|
||||
props<{ request: UserAccessPoliciesDialogRequest }>()
|
||||
);
|
||||
|
||||
export const promptDeleteUser = createAction(
|
||||
`${USER_PREFIX} Prompt Delete User`,
|
||||
props<{ request: DeleteUserRequest }>()
|
||||
);
|
||||
|
||||
export const deleteUser = createAction(`${USER_PREFIX} Delete User`, props<{ request: DeleteUserRequest }>());
|
||||
|
||||
export const promptDeleteUserGroup = createAction(
|
||||
`${USER_PREFIX} Prompt Delete User Group`,
|
||||
props<{ request: DeleteUserGroupRequest }>()
|
||||
);
|
||||
|
||||
export const deleteUserGroup = createAction(
|
||||
`${USER_PREFIX} Delete User Group`,
|
||||
props<{ request: DeleteUserGroupRequest }>()
|
||||
);
|
|
@ -0,0 +1,734 @@
|
|||
/*
|
||||
* 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 { NiFiState } from '../../../../state';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Router } from '@angular/router';
|
||||
import * as UserListingActions from './user-listing.actions';
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
filter,
|
||||
from,
|
||||
map,
|
||||
mergeMap,
|
||||
of,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
withLatestFrom
|
||||
} from 'rxjs';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { UsersService } from '../../service/users.service';
|
||||
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
|
||||
import { EditTenantDialog } from '../../../../ui/common/edit-tenant/edit-tenant-dialog.component';
|
||||
import { selectSaving, selectUserGroups, selectUsers } from './user-listing.selectors';
|
||||
import { EditTenantRequest, UserGroupEntity } from '../../../../state/shared';
|
||||
import { selectTenant } from './user-listing.actions';
|
||||
import { Client } from '../../../../service/client.service';
|
||||
import { NiFiCommon } from '../../../../service/nifi-common.service';
|
||||
import { UserAccessPolicies } from '../../ui/user-listing/user-access-policies/user-access-policies.component';
|
||||
|
||||
@Injectable()
|
||||
export class UserListingEffects {
|
||||
private requestId: number = 0;
|
||||
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private client: Client,
|
||||
private nifiCommon: NiFiCommon,
|
||||
private store: Store<NiFiState>,
|
||||
private router: Router,
|
||||
private usersService: UsersService,
|
||||
private dialog: MatDialog
|
||||
) {}
|
||||
|
||||
loadTenants$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.loadTenants),
|
||||
switchMap(() =>
|
||||
combineLatest([this.usersService.getUsers(), this.usersService.getUserGroups()]).pipe(
|
||||
map(([usersResponse, userGroupsResponse]) =>
|
||||
UserListingActions.loadTenantsSuccess({
|
||||
response: {
|
||||
users: usersResponse.users,
|
||||
userGroups: userGroupsResponse.userGroups,
|
||||
loadedTimestamp: usersResponse.generated
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
UserListingActions.usersApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
selectTenant$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.selectTenant),
|
||||
map((action) => action.id),
|
||||
tap((id) => {
|
||||
this.router.navigate(['/users', id]);
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
openCreateTenantDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.openCreateTenantDialog),
|
||||
withLatestFrom(this.store.select(selectUsers), this.store.select(selectUserGroups)),
|
||||
tap(([action, existingUsers, existingUserGroups]) => {
|
||||
const editTenantRequest: EditTenantRequest = {
|
||||
existingUsers,
|
||||
existingUserGroups
|
||||
};
|
||||
const dialogReference = this.dialog.open(EditTenantDialog, {
|
||||
data: editTenantRequest,
|
||||
panelClass: 'medium-dialog'
|
||||
});
|
||||
|
||||
dialogReference.componentInstance.saving$ = this.store.select(selectSaving);
|
||||
|
||||
dialogReference.componentInstance.editTenant
|
||||
.pipe(takeUntil(dialogReference.afterClosed()))
|
||||
.subscribe((response) => {
|
||||
if (response.user) {
|
||||
this.store.dispatch(
|
||||
UserListingActions.createUser({
|
||||
request: {
|
||||
revision: response.revision,
|
||||
userPayload: response.user.payload,
|
||||
userGroupUpdate: {
|
||||
requestId: this.requestId++,
|
||||
userGroups: response.user.userGroupsAdded
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} else if (response.userGroup) {
|
||||
const users: any[] = response.userGroup.users.map((id: string) => {
|
||||
return { id };
|
||||
});
|
||||
|
||||
this.store.dispatch(
|
||||
UserListingActions.createUserGroup({
|
||||
request: {
|
||||
revision: response.revision,
|
||||
userGroupPayload: {
|
||||
...response.userGroup.payload,
|
||||
users
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
createUser$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.createUser),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.usersService.createUser(request)).pipe(
|
||||
map((response) =>
|
||||
UserListingActions.createUserSuccess({
|
||||
response: {
|
||||
user: response,
|
||||
userGroupUpdate: request.userGroupUpdate
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
createUserSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.createUserSuccess),
|
||||
map((action) => action.response),
|
||||
withLatestFrom(this.store.select(selectUserGroups)),
|
||||
switchMap(([response, userGroups]) => {
|
||||
if (response.userGroupUpdate) {
|
||||
const userGroupUpdate = response.userGroupUpdate;
|
||||
const userGroupUpdates = [];
|
||||
|
||||
if (!this.nifiCommon.isEmpty(userGroupUpdate.userGroups)) {
|
||||
userGroupUpdates.push(
|
||||
...userGroupUpdate.userGroups
|
||||
.map((userGroupId: string) =>
|
||||
userGroups.find((userGroup) => userGroup.id == userGroupId)
|
||||
)
|
||||
.filter((userGroup) => userGroup != null)
|
||||
.map((userGroup) => {
|
||||
// @ts-ignore
|
||||
const ug: UserGroupEntity = userGroup;
|
||||
|
||||
const users: any[] = [
|
||||
...ug.component.users.map((user) => {
|
||||
return {
|
||||
id: user.id
|
||||
};
|
||||
}),
|
||||
{ id: response.user.id }
|
||||
];
|
||||
|
||||
return UserListingActions.updateUserGroup({
|
||||
request: {
|
||||
requestId: userGroupUpdate.requestId,
|
||||
id: ug.id,
|
||||
uri: ug.uri,
|
||||
revision: this.client.getRevision(userGroup),
|
||||
userGroupPayload: {
|
||||
...ug.component,
|
||||
users
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (userGroupUpdates.length === 0) {
|
||||
return of(UserListingActions.createUserComplete({ response }));
|
||||
} else {
|
||||
return userGroupUpdates;
|
||||
}
|
||||
} else {
|
||||
return of(UserListingActions.createUserComplete({ response }));
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
awaitUpdateUserGroupsForCreateUser$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.createUserSuccess),
|
||||
map((action) => action.response),
|
||||
filter((response) => response.userGroupUpdate != null),
|
||||
mergeMap((createUserResponse) =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.updateUserGroupSuccess),
|
||||
filter(
|
||||
(updateSuccess) =>
|
||||
// @ts-ignore
|
||||
createUserResponse.userGroupUpdate.requestId === updateSuccess.response.requestId
|
||||
),
|
||||
map((response) => UserListingActions.createUserComplete({ response: createUserResponse }))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
createUserComplete$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.createUserComplete),
|
||||
map((action) => action.response),
|
||||
tap((response) => {
|
||||
this.dialog.closeAll();
|
||||
this.store.dispatch(selectTenant({ id: response.user.id }));
|
||||
}),
|
||||
switchMap(() => of(UserListingActions.loadTenants()))
|
||||
)
|
||||
);
|
||||
|
||||
createUserGroup$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.createUserGroup),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.usersService.createUserGroup(request)).pipe(
|
||||
map((response) =>
|
||||
UserListingActions.createUserGroupSuccess({
|
||||
response: {
|
||||
userGroup: response
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
createUserGroupSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.createUserGroupSuccess),
|
||||
map((action) => action.response),
|
||||
tap((response) => {
|
||||
this.dialog.closeAll();
|
||||
this.store.dispatch(selectTenant({ id: response.userGroup.id }));
|
||||
}),
|
||||
switchMap(() => of(UserListingActions.loadTenants()))
|
||||
)
|
||||
);
|
||||
|
||||
navigateToEditTenant$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.navigateToEditTenant),
|
||||
map((action) => action.id),
|
||||
tap((id) => {
|
||||
this.router.navigate(['/users', id, 'edit']);
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
openConfigureUserDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.openConfigureUserDialog),
|
||||
map((action) => action.request),
|
||||
withLatestFrom(this.store.select(selectUsers), this.store.select(selectUserGroups)),
|
||||
tap(([request, existingUsers, existingUserGroups]) => {
|
||||
const editTenantRequest: EditTenantRequest = {
|
||||
user: request.user,
|
||||
existingUsers,
|
||||
existingUserGroups
|
||||
};
|
||||
const dialogReference = this.dialog.open(EditTenantDialog, {
|
||||
data: editTenantRequest,
|
||||
panelClass: 'medium-dialog'
|
||||
});
|
||||
|
||||
dialogReference.componentInstance.saving$ = this.store.select(selectSaving);
|
||||
|
||||
dialogReference.componentInstance.editTenant
|
||||
.pipe(takeUntil(dialogReference.afterClosed()))
|
||||
.subscribe((response) => {
|
||||
if (response.user) {
|
||||
const userGroupsAdded: string[] = response.user.userGroupsAdded;
|
||||
const userGroupsRemoved: string[] = response.user.userGroupsRemoved;
|
||||
|
||||
if (
|
||||
this.nifiCommon.isEmpty(userGroupsAdded) &&
|
||||
this.nifiCommon.isEmpty(userGroupsRemoved)
|
||||
) {
|
||||
this.store.dispatch(
|
||||
UserListingActions.updateUser({
|
||||
request: {
|
||||
revision: response.revision,
|
||||
id: request.user.id,
|
||||
uri: request.user.uri,
|
||||
userPayload: {
|
||||
...request.user.component,
|
||||
...response.user.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
UserListingActions.updateUser({
|
||||
request: {
|
||||
revision: response.revision,
|
||||
id: request.user.id,
|
||||
uri: request.user.uri,
|
||||
userPayload: {
|
||||
...request.user.component,
|
||||
...response.user.payload
|
||||
},
|
||||
userGroupUpdate: {
|
||||
requestId: this.requestId++,
|
||||
userGroupsAdded: response.user.userGroupsAdded,
|
||||
userGroupsRemoved: response.user.userGroupsRemoved
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialogReference.afterClosed().subscribe((response) => {
|
||||
this.store.dispatch(
|
||||
selectTenant({
|
||||
id: request.user.id
|
||||
})
|
||||
);
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
updateUser$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.updateUser),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.usersService.updateUser(request)).pipe(
|
||||
map((response) =>
|
||||
UserListingActions.updateUserSuccess({
|
||||
response: {
|
||||
user: response,
|
||||
userGroupUpdate: request.userGroupUpdate
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
updateUserSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.updateUserSuccess),
|
||||
map((action) => action.response),
|
||||
withLatestFrom(this.store.select(selectUserGroups)),
|
||||
switchMap(([response, userGroups]) => {
|
||||
if (response.userGroupUpdate) {
|
||||
const userGroupUpdate = response.userGroupUpdate;
|
||||
const userGroupUpdates = [];
|
||||
|
||||
if (!this.nifiCommon.isEmpty(userGroupUpdate.userGroupsAdded)) {
|
||||
userGroupUpdates.push(
|
||||
...userGroupUpdate.userGroupsAdded
|
||||
.map((userGroupId: string) =>
|
||||
userGroups.find((userGroup) => userGroup.id == userGroupId)
|
||||
)
|
||||
.filter((userGroup) => userGroup != null)
|
||||
.map((userGroup) => {
|
||||
// @ts-ignore
|
||||
const ug: UserGroupEntity = userGroup;
|
||||
|
||||
const users: any[] = [
|
||||
...ug.component.users.map((user) => {
|
||||
return {
|
||||
id: user.id
|
||||
};
|
||||
}),
|
||||
{ id: response.user.id }
|
||||
];
|
||||
|
||||
return UserListingActions.updateUserGroup({
|
||||
request: {
|
||||
requestId: userGroupUpdate.requestId,
|
||||
revision: this.client.getRevision(userGroup),
|
||||
id: ug.id,
|
||||
uri: ug.uri,
|
||||
userGroupPayload: {
|
||||
...ug.component,
|
||||
users
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.nifiCommon.isEmpty(userGroupUpdate.userGroupsRemoved)) {
|
||||
userGroupUpdates.push(
|
||||
...userGroupUpdate.userGroupsRemoved
|
||||
.map((userGroupId: string) =>
|
||||
userGroups.find((userGroup) => userGroup.id == userGroupId)
|
||||
)
|
||||
.filter((userGroup) => userGroup != null)
|
||||
.map((userGroup) => {
|
||||
// @ts-ignore
|
||||
const ug: UserGroupEntity = userGroup;
|
||||
|
||||
const users: any[] = [
|
||||
...ug.component.users
|
||||
.filter((user) => user.id != response.user.id)
|
||||
.map((user) => {
|
||||
return {
|
||||
id: user.id
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
return UserListingActions.updateUserGroup({
|
||||
request: {
|
||||
requestId: userGroupUpdate.requestId,
|
||||
revision: this.client.getRevision(userGroup),
|
||||
id: ug.id,
|
||||
uri: ug.uri,
|
||||
userGroupPayload: {
|
||||
...ug.component,
|
||||
users
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (userGroupUpdates.length === 0) {
|
||||
return of(UserListingActions.updateUserComplete());
|
||||
} else {
|
||||
return userGroupUpdates;
|
||||
}
|
||||
} else {
|
||||
return of(UserListingActions.updateUserComplete());
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
awaitUpdateUserGroupsForUpdateUser$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.updateUserSuccess),
|
||||
map((action) => action.response),
|
||||
filter((response) => response.userGroupUpdate != null),
|
||||
mergeMap((updateUserResponse) =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.updateUserGroupSuccess),
|
||||
filter(
|
||||
(updateSuccess) =>
|
||||
// @ts-ignore
|
||||
updateUserResponse.userGroupUpdate.requestId === updateSuccess.response.requestId
|
||||
),
|
||||
map((response) => UserListingActions.updateUserComplete())
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
updateUserComplete$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.updateUserComplete),
|
||||
tap(() => {
|
||||
this.dialog.closeAll();
|
||||
}),
|
||||
switchMap((request) => of(UserListingActions.loadTenants()))
|
||||
)
|
||||
);
|
||||
|
||||
openConfigureUserGroupDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.openConfigureUserGroupDialog),
|
||||
map((action) => action.request),
|
||||
withLatestFrom(this.store.select(selectUsers), this.store.select(selectUserGroups)),
|
||||
tap(([request, existingUsers, existingUserGroups]) => {
|
||||
const editTenantRequest: EditTenantRequest = {
|
||||
userGroup: request.userGroup,
|
||||
existingUsers,
|
||||
existingUserGroups
|
||||
};
|
||||
const dialogReference = this.dialog.open(EditTenantDialog, {
|
||||
data: editTenantRequest,
|
||||
panelClass: 'medium-dialog'
|
||||
});
|
||||
|
||||
dialogReference.componentInstance.saving$ = this.store.select(selectSaving);
|
||||
|
||||
dialogReference.componentInstance.editTenant
|
||||
.pipe(takeUntil(dialogReference.afterClosed()))
|
||||
.subscribe((response) => {
|
||||
if (response.userGroup) {
|
||||
const users: any[] = response.userGroup.users.map((id: string) => {
|
||||
return { id };
|
||||
});
|
||||
|
||||
this.store.dispatch(
|
||||
UserListingActions.updateUserGroup({
|
||||
request: {
|
||||
revision: response.revision,
|
||||
id: response.userGroup.id,
|
||||
uri: request.userGroup.uri,
|
||||
userGroupPayload: {
|
||||
...request.userGroup.component,
|
||||
...response.userGroup.payload,
|
||||
users
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
dialogReference.afterClosed().subscribe(() => {
|
||||
this.store.dispatch(
|
||||
selectTenant({
|
||||
id: request.userGroup.id
|
||||
})
|
||||
);
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
updateUserGroup$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.updateUserGroup),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.usersService.updateUserGroup(request)).pipe(
|
||||
map((response) =>
|
||||
UserListingActions.updateUserGroupSuccess({
|
||||
response: {
|
||||
requestId: request.requestId,
|
||||
userGroup: response
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
updateUserGroupSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.updateUserGroupSuccess),
|
||||
map((action) => action.response),
|
||||
filter((response) => response.requestId == null),
|
||||
tap(() => {
|
||||
this.dialog.closeAll();
|
||||
}),
|
||||
switchMap((request) => of(UserListingActions.loadTenants()))
|
||||
)
|
||||
);
|
||||
|
||||
navigateToViewAccessPolicies$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.navigateToViewAccessPolicies),
|
||||
map((action) => action.id),
|
||||
tap((id) => {
|
||||
this.router.navigate(['/users', id, 'policies']);
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
openUserAccessPoliciesDialog = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.openUserAccessPoliciesDialog),
|
||||
map((action) => action.request),
|
||||
tap((request) => {
|
||||
this.dialog
|
||||
.open(UserAccessPolicies, {
|
||||
data: request,
|
||||
panelClass: 'large-dialog'
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((response) => {
|
||||
if (response != 'ROUTED') {
|
||||
this.store.dispatch(
|
||||
selectTenant({
|
||||
id: request.id
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
promptDeleteUser$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.promptDeleteUser),
|
||||
map((action) => action.request),
|
||||
tap((request) => {
|
||||
const dialogReference = this.dialog.open(YesNoDialog, {
|
||||
data: {
|
||||
title: 'Delete User Account',
|
||||
message: `Are you sure you want to delete the user account for '${request.user.component.identity}'?`
|
||||
},
|
||||
panelClass: 'small-dialog'
|
||||
});
|
||||
|
||||
dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
|
||||
this.store.dispatch(
|
||||
UserListingActions.deleteUser({
|
||||
request
|
||||
})
|
||||
);
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
deleteUser$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.deleteUser),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.usersService.deleteUser(request.user)).pipe(
|
||||
map((response) => UserListingActions.loadTenants()),
|
||||
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
promptDeleteUserGroup$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.promptDeleteUserGroup),
|
||||
map((action) => action.request),
|
||||
tap((request) => {
|
||||
const dialogReference = this.dialog.open(YesNoDialog, {
|
||||
data: {
|
||||
title: 'Delete User Account',
|
||||
message: `Are you sure you want to delete the user group account for '${request.userGroup.component.identity}'?`
|
||||
},
|
||||
panelClass: 'small-dialog'
|
||||
});
|
||||
|
||||
dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
|
||||
this.store.dispatch(
|
||||
UserListingActions.deleteUserGroup({
|
||||
request
|
||||
})
|
||||
);
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
deleteUserGroup$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserListingActions.deleteUserGroup),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.usersService.deleteUserGroup(request.userGroup)).pipe(
|
||||
map(() => UserListingActions.loadTenants()),
|
||||
catchError((error) => of(UserListingActions.usersApiError({ error: error.error })))
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
|
@ -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 { UserListingState } from './index';
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import {
|
||||
createUser,
|
||||
createUserComplete,
|
||||
createUserGroup,
|
||||
createUserGroupSuccess,
|
||||
loadTenants,
|
||||
loadTenantsSuccess,
|
||||
resetUsersState,
|
||||
updateUser,
|
||||
updateUserComplete,
|
||||
updateUserGroup,
|
||||
updateUserGroupSuccess
|
||||
} from './user-listing.actions';
|
||||
|
||||
export const initialState: UserListingState = {
|
||||
users: [],
|
||||
userGroups: [],
|
||||
saving: false,
|
||||
loadedTimestamp: '',
|
||||
error: null,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
export const userListingReducer = createReducer(
|
||||
initialState,
|
||||
on(resetUsersState, (state) => ({
|
||||
...initialState
|
||||
})),
|
||||
on(loadTenants, (state) => ({
|
||||
...state,
|
||||
status: 'loading' as const
|
||||
})),
|
||||
on(loadTenantsSuccess, (state, { response }) => ({
|
||||
...state,
|
||||
users: response.users,
|
||||
userGroups: response.userGroups,
|
||||
loadedTimestamp: response.loadedTimestamp,
|
||||
error: null,
|
||||
status: 'success' as const
|
||||
})),
|
||||
on(createUser, updateUser, createUserGroup, updateUserGroup, (state) => ({
|
||||
...state,
|
||||
saving: true
|
||||
})),
|
||||
on(createUserComplete, (state) => ({
|
||||
...state,
|
||||
saving: false
|
||||
})),
|
||||
on(updateUserComplete, (state) => ({
|
||||
...state,
|
||||
saving: false
|
||||
})),
|
||||
on(createUserGroupSuccess, (state, { response }) => ({
|
||||
...state,
|
||||
saving: false
|
||||
})),
|
||||
on(updateUserGroupSuccess, (state, { response }) => ({
|
||||
...state,
|
||||
saving: response.requestId == null ? false : state.saving
|
||||
}))
|
||||
);
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import { createSelector } from '@ngrx/store';
|
||||
import { usersFeatureKey, UsersState, selectUserState } from '../index';
|
||||
import { SelectedTenant, UserListingState } from './index';
|
||||
import { selectCurrentRoute } from '../../../../state/router/router.selectors';
|
||||
|
||||
export const selectUserListingState = createSelector(selectUserState, (state: UsersState) => state[usersFeatureKey]);
|
||||
|
||||
export const selectSaving = createSelector(selectUserListingState, (state: UserListingState) => state.saving);
|
||||
|
||||
export const selectUsers = createSelector(selectUserListingState, (state: UserListingState) => state.users);
|
||||
|
||||
export const selectUserGroups = createSelector(selectUserListingState, (state: UserListingState) => state.userGroups);
|
||||
|
||||
export const selectTenantIdFromRoute = createSelector(selectCurrentRoute, (route) => {
|
||||
if (route) {
|
||||
return route.params.id;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
export const selectSingleEditedTenant = createSelector(selectCurrentRoute, (route) => {
|
||||
if (route?.routeConfig?.path == 'edit') {
|
||||
return route.params.id;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
export const selectTenantForAccessPolicies = createSelector(selectCurrentRoute, (route) => {
|
||||
if (route?.routeConfig?.path == 'policies') {
|
||||
return route.params.id;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
export const selectSelectedTenant = (id: string) =>
|
||||
createSelector(selectUserListingState, (state: UserListingState) => {
|
||||
const user = state.users.find((user) => id == user.id);
|
||||
if (user) {
|
||||
return {
|
||||
id,
|
||||
user
|
||||
} as SelectedTenant;
|
||||
}
|
||||
const userGroup = state.userGroups.find((userGroup) => id == userGroup.id);
|
||||
if (userGroup) {
|
||||
return {
|
||||
id,
|
||||
userGroup
|
||||
} as SelectedTenant;
|
||||
}
|
||||
return null;
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
<!--
|
||||
~ 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="user-access-policies" tabindex="0">
|
||||
<h3 mat-dialog-title>User Policies</h3>
|
||||
<mat-dialog-content>
|
||||
<div class="flex flex-col justify-between gap-y-3">
|
||||
<div class="flex flex-col">
|
||||
<div>User</div>
|
||||
<div class="value">{{ request.identity }}</div>
|
||||
</div>
|
||||
<div class="listing-table">
|
||||
<div class="h-96 overflow-y-auto overflow-x-hidden border">
|
||||
<table
|
||||
mat-table
|
||||
[dataSource]="dataSource"
|
||||
matSort
|
||||
matSortDisableClear
|
||||
(matSortChange)="updateSort($event)"
|
||||
[matSortActive]="sort.active"
|
||||
[matSortDirection]="sort.direction">
|
||||
<!-- Policy Column -->
|
||||
<ng-container matColumnDef="policy">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Policy</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div *ngIf="item.permissions.canRead; else noPermissions">
|
||||
{{ formatPolicy(item) }}
|
||||
</div>
|
||||
<ng-template #noPermissions>
|
||||
<div class="unset">{{ item.id }}</div>
|
||||
</ng-template>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Action Column -->
|
||||
<ng-container matColumnDef="action">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Action</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.component.action }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div
|
||||
class="pointer fa fa-long-arrow-right"
|
||||
*ngIf="canGoToPolicyTarget(item)"
|
||||
[routerLink]="getPolicyTargetLink(item)"
|
||||
mat-dialog-close="ROUTED"
|
||||
title="Go to"></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)="selectPolicy(row)"
|
||||
[class.selected]="isSelected(row)"
|
||||
[class.even]="even"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value">
|
||||
Some policies may be inherited by descendant components unless explicitly overridden.
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button color="primary" mat-raised-button mat-dialog-close>Close</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.user-access-policies {
|
||||
@include mat.button-density(-1);
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
.listing-table {
|
||||
table {
|
||||
width: auto;
|
||||
|
||||
td,
|
||||
th {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mat-column-action {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.mat-column-actions {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { UserAccessPolicies } from './user-access-policies.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { UserAccessPoliciesDialogRequest } from '../../../state/user-listing';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
describe('UserAccessPolicies', () => {
|
||||
let component: UserAccessPolicies;
|
||||
let fixture: ComponentFixture<UserAccessPolicies>;
|
||||
|
||||
const data: UserAccessPoliciesDialogRequest = {
|
||||
id: 'acfbfa2c-018c-1000-0311-47b83e34c9c3',
|
||||
identity: 'group 1',
|
||||
accessPolicies: [
|
||||
{
|
||||
revision: {
|
||||
clientId: 'b09bd713-018c-1000-e5b8-14855e466f1b',
|
||||
version: 4
|
||||
},
|
||||
id: 'b0c3148d-018c-1000-2cfe-8fab902c11f7',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'b0c3148d-018c-1000-2cfe-8fab902c11f7',
|
||||
resource: '/system',
|
||||
action: 'read',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [UserAccessPolicies, BrowserAnimationsModule],
|
||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
|
||||
});
|
||||
fixture = TestBed.createComponent(UserAccessPolicies);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule, Sort } from '@angular/material/sort';
|
||||
import { NgIf } from '@angular/common';
|
||||
import {
|
||||
AccessPolicySummaryEntity,
|
||||
ComponentReferenceEntity,
|
||||
ComponentType,
|
||||
SelectOption
|
||||
} from '../../../../../state/shared';
|
||||
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { UserAccessPoliciesDialogRequest } from '../../../state/user-listing';
|
||||
|
||||
@Component({
|
||||
selector: 'user-access-policies',
|
||||
standalone: true,
|
||||
templateUrl: './user-access-policies.component.html',
|
||||
imports: [MatButtonModule, MatDialogModule, MatTableModule, MatSortModule, NgIf, RouterLink],
|
||||
styleUrls: ['./user-access-policies.component.scss', '../../../../../../assets/styles/listing-table.scss']
|
||||
})
|
||||
export class UserAccessPolicies {
|
||||
displayedColumns: string[] = ['policy', 'action', 'actions'];
|
||||
dataSource: MatTableDataSource<AccessPolicySummaryEntity> = new MatTableDataSource<AccessPolicySummaryEntity>();
|
||||
selectedPolicyId: string | null = null;
|
||||
|
||||
sort: Sort = {
|
||||
active: 'policy',
|
||||
direction: 'asc'
|
||||
};
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public request: UserAccessPoliciesDialogRequest,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {
|
||||
this.dataSource.data = this.sortPolicies(request.accessPolicies, this.sort);
|
||||
}
|
||||
|
||||
updateSort(sort: Sort): void {
|
||||
this.sort = sort;
|
||||
this.dataSource.data = this.sortPolicies(this.dataSource.data, sort);
|
||||
}
|
||||
|
||||
sortPolicies(policies: AccessPolicySummaryEntity[], sort: Sort): AccessPolicySummaryEntity[] {
|
||||
const data: AccessPolicySummaryEntity[] = policies.slice();
|
||||
return data.sort((a, b) => {
|
||||
const isAsc = sort.direction === 'asc';
|
||||
|
||||
let retVal: number = 0;
|
||||
if (a.permissions.canRead && b.permissions.canRead) {
|
||||
switch (sort.active) {
|
||||
case 'policy':
|
||||
retVal = this.nifiCommon.compareString(this.formatPolicy(a), this.formatPolicy(b));
|
||||
break;
|
||||
case 'action':
|
||||
retVal = this.nifiCommon.compareString(a.component.action, b.component.action);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (!a.permissions.canRead && !b.permissions.canRead) {
|
||||
retVal = 0;
|
||||
}
|
||||
if (a.permissions.canRead) {
|
||||
retVal = 1;
|
||||
} else {
|
||||
retVal = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal * (isAsc ? 1 : -1);
|
||||
});
|
||||
}
|
||||
|
||||
formatPolicy(policy: AccessPolicySummaryEntity): string {
|
||||
if (policy.component.resource.startsWith('/restricted-components')) {
|
||||
// restricted components policy
|
||||
return this.restrictedComponentResourceParser(policy);
|
||||
} else if (policy.component.componentReference) {
|
||||
// not restricted/global policy... check if user has access to the component reference
|
||||
return this.componentResourceParser(policy);
|
||||
} else {
|
||||
// may be a global policy
|
||||
const policyValue: string = this.nifiCommon.substringAfterLast(policy.component.resource, '/');
|
||||
const policyOption: SelectOption | undefined = this.nifiCommon.getPolicyTypeListing(policyValue);
|
||||
|
||||
// if known global policy, format it otherwise format as unknown
|
||||
if (policyOption) {
|
||||
return this.globalResourceParser(policyOption);
|
||||
} else {
|
||||
return this.unknownResourceParser(policy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a human-readable restricted component policy string.
|
||||
*
|
||||
* @returns {string}
|
||||
* @param policy
|
||||
*/
|
||||
private restrictedComponentResourceParser(policy: AccessPolicySummaryEntity): string {
|
||||
const resource: string = policy.component.resource;
|
||||
|
||||
if (resource === '/restricted-components') {
|
||||
return 'Restricted components regardless of restrictions';
|
||||
}
|
||||
|
||||
var subResource = this.nifiCommon.substringAfterFirst(resource, '/restricted-components/');
|
||||
return `Restricted components requiring '${subResource}'`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a human-readable component policy string.
|
||||
*
|
||||
* @returns {string}
|
||||
* @param policy
|
||||
*/
|
||||
private componentResourceParser(policy: AccessPolicySummaryEntity): string {
|
||||
let resource: string = policy.component.resource;
|
||||
let policyLabel: string = '';
|
||||
|
||||
// determine policy type
|
||||
if (resource.startsWith('/policies')) {
|
||||
resource = this.nifiCommon.substringAfterFirst(resource, '/policies');
|
||||
policyLabel += 'Admin policy for ';
|
||||
} else if (resource.startsWith('/data-transfer')) {
|
||||
resource = this.nifiCommon.substringAfterFirst(resource, '/data-transfer');
|
||||
policyLabel += 'Site to site policy for ';
|
||||
} else if (resource.startsWith('/data')) {
|
||||
resource = this.nifiCommon.substringAfterFirst(resource, '/data');
|
||||
policyLabel += 'Data policy for ';
|
||||
} else if (resource.startsWith('/operation')) {
|
||||
resource = this.nifiCommon.substringAfterFirst(resource, '/operation');
|
||||
policyLabel += 'Operate policy for ';
|
||||
} else {
|
||||
policyLabel += 'Component policy for ';
|
||||
}
|
||||
|
||||
if (resource.startsWith('/processors')) {
|
||||
policyLabel += 'processor ';
|
||||
} else if (resource.startsWith('/controller-services')) {
|
||||
policyLabel += 'controller service ';
|
||||
} else if (resource.startsWith('/funnels')) {
|
||||
policyLabel += 'funnel ';
|
||||
} else if (resource.startsWith('/input-ports')) {
|
||||
policyLabel += 'input port ';
|
||||
} else if (resource.startsWith('/labels')) {
|
||||
policyLabel += 'label ';
|
||||
} else if (resource.startsWith('/output-ports')) {
|
||||
policyLabel += 'output port ';
|
||||
} else if (resource.startsWith('/process-groups')) {
|
||||
policyLabel += 'process group ';
|
||||
} else if (resource.startsWith('/remote-process-groups')) {
|
||||
policyLabel += 'remote process group ';
|
||||
} else if (resource.startsWith('/reporting-tasks')) {
|
||||
policyLabel += 'reporting task ';
|
||||
} else if (resource.startsWith('/parameter-contexts')) {
|
||||
policyLabel += 'parameter context ';
|
||||
}
|
||||
|
||||
const componentReference: ComponentReferenceEntity | undefined = policy.component.componentReference;
|
||||
if (componentReference) {
|
||||
if (componentReference.permissions.canRead) {
|
||||
policyLabel += componentReference.component.name;
|
||||
} else {
|
||||
policyLabel += componentReference.id;
|
||||
}
|
||||
}
|
||||
|
||||
return policyLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a human-readable global policy string.
|
||||
*
|
||||
* @param policy
|
||||
* @returns {string}
|
||||
*/
|
||||
globalResourceParser(policy: SelectOption): string {
|
||||
return `Global policy to ${policy.text}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a human-readable policy string for an unknown resource.
|
||||
*
|
||||
* @returns {string}
|
||||
* @param policy
|
||||
*/
|
||||
unknownResourceParser(policy: AccessPolicySummaryEntity): string {
|
||||
return `Unknown resource ${policy.component.resource}`;
|
||||
}
|
||||
|
||||
canGoToPolicyTarget(policy: AccessPolicySummaryEntity): boolean {
|
||||
return policy.permissions.canRead && policy.component.componentReference != null;
|
||||
}
|
||||
|
||||
getPolicyTargetLink(policy: AccessPolicySummaryEntity): string[] {
|
||||
const resource: string = policy.component.resource;
|
||||
|
||||
// @ts-ignore
|
||||
const componentReference: ComponentReferenceEntity = policy.component.componentReference;
|
||||
|
||||
if (resource.indexOf('/processors') >= 0) {
|
||||
return [
|
||||
'/process-groups',
|
||||
// @ts-ignore
|
||||
componentReference.parentGroupId,
|
||||
ComponentType.Processor,
|
||||
componentReference.id
|
||||
];
|
||||
} else if (resource.indexOf('/controller-services') >= 0) {
|
||||
if (componentReference.parentGroupId) {
|
||||
return [
|
||||
'/process-groups',
|
||||
componentReference.parentGroupId,
|
||||
'controller-services',
|
||||
componentReference.id
|
||||
];
|
||||
} else {
|
||||
return ['/settings', 'management-controller-services', componentReference.id];
|
||||
}
|
||||
} else if (resource.indexOf('/funnels') >= 0) {
|
||||
// @ts-ignore
|
||||
return ['/process-groups', componentReference.parentGroupId, ComponentType.Funnel, componentReference.id];
|
||||
} else if (resource.indexOf('/input-ports') >= 0) {
|
||||
return [
|
||||
'/process-groups',
|
||||
// @ts-ignore
|
||||
componentReference.parentGroupId,
|
||||
ComponentType.InputPort,
|
||||
componentReference.id
|
||||
];
|
||||
} else if (resource.indexOf('/labels') >= 0) {
|
||||
// @ts-ignore
|
||||
return ['/process-groups', componentReference.parentGroupId, ComponentType.Label, componentReference.id];
|
||||
} else if (resource.indexOf('/output-ports') >= 0) {
|
||||
return [
|
||||
'/process-groups',
|
||||
// @ts-ignore
|
||||
componentReference.parentGroupId,
|
||||
ComponentType.OutputPort,
|
||||
componentReference.id
|
||||
];
|
||||
} else if (resource.indexOf('/process-groups') >= 0) {
|
||||
if (componentReference.parentGroupId) {
|
||||
return [
|
||||
'/process-groups',
|
||||
componentReference.parentGroupId,
|
||||
ComponentType.ProcessGroup,
|
||||
componentReference.id
|
||||
];
|
||||
} else {
|
||||
return ['/process-groups', componentReference.id];
|
||||
}
|
||||
} else if (resource.indexOf('/remote-process-groups') >= 0) {
|
||||
return [
|
||||
'/process-groups',
|
||||
// @ts-ignore
|
||||
componentReference.parentGroupId,
|
||||
ComponentType.RemoteProcessGroup,
|
||||
componentReference.id
|
||||
];
|
||||
} else if (resource.indexOf('/reporting-tasks') >= 0) {
|
||||
return ['/settings', 'reporting-tasks', componentReference.id];
|
||||
} else if (resource.indexOf('/parameter-contexts') >= 0) {
|
||||
return ['/parameter-contexts', componentReference.id];
|
||||
}
|
||||
return ['/'];
|
||||
}
|
||||
|
||||
selectPolicy(policy: AccessPolicySummaryEntity): void {
|
||||
this.selectedPolicyId = policy.id;
|
||||
}
|
||||
|
||||
isSelected(policy: AccessPolicySummaryEntity): boolean {
|
||||
if (this.selectedPolicyId) {
|
||||
return policy.id == this.selectedPolicyId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<ng-container *ngIf="userListingState$ | async; let userListingState">
|
||||
<div *ngIf="isInitialLoading(userListingState); else loaded">
|
||||
<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-1" *ngIf="currentUser$ | async as user">
|
||||
<user-table
|
||||
[tenants]="{ users: userListingState.users, userGroups: userListingState.userGroups }"
|
||||
[selectedTenantId]="selectedTenantId$ | async"
|
||||
[currentUser]="(currentUser$ | async)!"
|
||||
[configurableUsersAndGroups]="true"
|
||||
(createTenant)="createTenant()"
|
||||
(selectTenant)="selectTenant($event)"
|
||||
(editTenant)="editTenant($event)"
|
||||
(deleteUser)="deleteUser($event)"
|
||||
(deleteUserGroup)="deleteUserGroup($event)"
|
||||
(viewAccessPolicies)="viewAccessPolicies($event)"></user-table>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="refresh-container flex items-center gap-x-2">
|
||||
<button class="nifi-button" (click)="refreshUserListing()">
|
||||
<i class="fa fa-refresh" [class.fa-spin]="userListingState.status === 'loading'"></i>
|
||||
</button>
|
||||
<div>Last updated:</div>
|
||||
<div class="refresh-timestamp">{{ userListingState.loadedTimestamp }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
|
@ -0,0 +1,16 @@
|
|||
/*!
|
||||
* 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.
|
||||
*/
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { UserListing } from './user-listing.component';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../state/user-listing/user-listing.reducer';
|
||||
|
||||
describe('UserListing', () => {
|
||||
let component: UserListing;
|
||||
let fixture: ComponentFixture<UserListing>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [UserListing],
|
||||
providers: [provideMockStore({ initialState })]
|
||||
});
|
||||
fixture = TestBed.createComponent(UserListing);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||
import { UserListingState } from '../../state/user-listing';
|
||||
import {
|
||||
selectSelectedTenant,
|
||||
selectSingleEditedTenant,
|
||||
selectTenantForAccessPolicies,
|
||||
selectTenantIdFromRoute,
|
||||
selectUserListingState
|
||||
} from '../../state/user-listing/user-listing.selectors';
|
||||
import { initialState } from '../../state/user-listing/user-listing.reducer';
|
||||
import {
|
||||
openCreateTenantDialog,
|
||||
loadTenants,
|
||||
navigateToEditTenant,
|
||||
navigateToViewAccessPolicies,
|
||||
openConfigureUserDialog,
|
||||
openConfigureUserGroupDialog,
|
||||
openUserAccessPoliciesDialog,
|
||||
promptDeleteUser,
|
||||
promptDeleteUserGroup,
|
||||
selectTenant
|
||||
} from '../../state/user-listing/user-listing.actions';
|
||||
import { filter, switchMap, take } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { UserEntity, UserGroupEntity } from '../../../../state/shared';
|
||||
|
||||
@Component({
|
||||
selector: 'user-listing',
|
||||
templateUrl: './user-listing.component.html',
|
||||
styleUrls: ['./user-listing.component.scss']
|
||||
})
|
||||
export class UserListing implements OnInit {
|
||||
userListingState$ = this.store.select(selectUserListingState);
|
||||
selectedTenantId$ = this.store.select(selectTenantIdFromRoute);
|
||||
currentUser$ = this.store.select(selectCurrentUser);
|
||||
|
||||
constructor(private store: Store<UserListingState>) {
|
||||
this.store
|
||||
.select(selectSingleEditedTenant)
|
||||
.pipe(
|
||||
filter((id: string) => id != null),
|
||||
switchMap((id: string) =>
|
||||
this.store.select(selectSelectedTenant(id)).pipe(
|
||||
filter((entity) => entity != null),
|
||||
take(1)
|
||||
)
|
||||
),
|
||||
takeUntilDestroyed()
|
||||
)
|
||||
.subscribe((selectedTenant) => {
|
||||
if (selectedTenant?.user) {
|
||||
this.store.dispatch(
|
||||
openConfigureUserDialog({
|
||||
request: {
|
||||
user: selectedTenant.user
|
||||
}
|
||||
})
|
||||
);
|
||||
} else if (selectedTenant?.userGroup) {
|
||||
this.store.dispatch(
|
||||
openConfigureUserGroupDialog({
|
||||
request: {
|
||||
userGroup: selectedTenant.userGroup
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.store
|
||||
.select(selectTenantForAccessPolicies)
|
||||
.pipe(
|
||||
filter((id: string) => id != null),
|
||||
switchMap((id: string) =>
|
||||
this.store.select(selectSelectedTenant(id)).pipe(
|
||||
filter((entity) => entity != null),
|
||||
take(1)
|
||||
)
|
||||
),
|
||||
takeUntilDestroyed()
|
||||
)
|
||||
.subscribe((selectedTenant) => {
|
||||
if (selectedTenant?.user) {
|
||||
this.store.dispatch(
|
||||
openUserAccessPoliciesDialog({
|
||||
request: {
|
||||
id: selectedTenant.user.id,
|
||||
identity: selectedTenant.user.component.identity,
|
||||
accessPolicies: selectedTenant.user.component.accessPolicies
|
||||
}
|
||||
})
|
||||
);
|
||||
} else if (selectedTenant?.userGroup) {
|
||||
this.store.dispatch(
|
||||
openUserAccessPoliciesDialog({
|
||||
request: {
|
||||
id: selectedTenant.userGroup.id,
|
||||
identity: selectedTenant.userGroup.component.identity,
|
||||
accessPolicies: selectedTenant.userGroup.component.accessPolicies
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(loadTenants());
|
||||
}
|
||||
|
||||
isInitialLoading(state: UserListingState): boolean {
|
||||
return state.loadedTimestamp == initialState.loadedTimestamp;
|
||||
}
|
||||
|
||||
createTenant(): void {
|
||||
this.store.dispatch(openCreateTenantDialog());
|
||||
}
|
||||
|
||||
selectTenant(id: string): void {
|
||||
this.store.dispatch(
|
||||
selectTenant({
|
||||
id
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
editTenant(id: string): void {
|
||||
this.store.dispatch(
|
||||
navigateToEditTenant({
|
||||
id
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteUser(user: UserEntity): void {
|
||||
this.store.dispatch(
|
||||
promptDeleteUser({
|
||||
request: {
|
||||
user
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteUserGroup(userGroup: UserGroupEntity): void {
|
||||
this.store.dispatch(
|
||||
promptDeleteUserGroup({
|
||||
request: {
|
||||
userGroup
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
viewAccessPolicies(id: string): void {
|
||||
this.store.dispatch(
|
||||
navigateToViewAccessPolicies({
|
||||
id
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
refreshUserListing() {
|
||||
this.store.dispatch(loadTenants());
|
||||
}
|
||||
}
|
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { UserListing } from './user-listing.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { UserTable } from './user-table/user-table.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [UserListing, UserTable],
|
||||
exports: [UserListing],
|
||||
imports: [
|
||||
CommonModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
MatSelectModule
|
||||
]
|
||||
})
|
||||
export class UserListingModule {}
|
|
@ -0,0 +1,106 @@
|
|||
<!--
|
||||
~ 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="user-table h-full flex flex-col">
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<div class="value">Displaying {{ filteredCount }} of {{ totalCount }}</div>
|
||||
<form [formGroup]="filterForm">
|
||||
<div class="flex pt-2">
|
||||
<div class="mr-2">
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<input matInput type="text" class="small" formControlName="filterTerm" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<mat-label>Filter By</mat-label>
|
||||
<mat-select formControlName="filterColumn">
|
||||
<mat-option value="user"> user</mat-option>
|
||||
<mat-option value="membership"> membership</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
<button *ngIf="canModifyTenants(currentUser)" class="nifi-button" (click)="createClicked()">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 relative">
|
||||
<div class="listing-table overflow-y-auto border absolute inset-0">
|
||||
<table
|
||||
mat-table
|
||||
[dataSource]="dataSource"
|
||||
matSort
|
||||
matSortDisableClear
|
||||
(matSortChange)="updateSort($event)"
|
||||
[matSortActive]="sort.active"
|
||||
[matSortDirection]="sort.direction">
|
||||
<!-- User column -->
|
||||
<ng-container matColumnDef="user">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>User</th>
|
||||
<td mat-cell *matCellDef="let item" class="items-center">
|
||||
<i *ngIf="item.tenantType === 'userGroup'" class="fa fa-users mr-3"></i>{{ item.user }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Membership Column -->
|
||||
<ng-container matColumnDef="membership">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Membership</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.tenantType === 'user' ? 'Member of' : 'Members' }}: {{ formatMembership(item) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<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"
|
||||
title="Edit"
|
||||
*ngIf="canEditOrDelete(currentUser, item)"
|
||||
(click)="editClicked(item, $event)"></div>
|
||||
<div
|
||||
class="pointer fa fa-trash"
|
||||
title="Remove"
|
||||
*ngIf="canEditOrDelete(currentUser, item)"
|
||||
(click)="deleteClicked(item)"></div>
|
||||
<div
|
||||
class="pointer fa fa-key"
|
||||
title="View User Policies"
|
||||
*ngIf="hasAccessPolicies(item)"
|
||||
(click)="viewAccessPoliciesClicked(item, $event)"></div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; let even = even; columns: displayedColumns"
|
||||
(click)="select(row)"
|
||||
[class.selected]="isSelected(row)"
|
||||
[class.even]="even"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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.
|
||||
*/
|
||||
|
||||
.user-table {
|
||||
.listing-table {
|
||||
.mat-column-actions {
|
||||
width: 75px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* 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 { UserTable } from './user-table.component';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { CurrentUser } from '../../../../../state/current-user';
|
||||
|
||||
describe('UserTable', () => {
|
||||
let component: UserTable;
|
||||
let fixture: ComponentFixture<UserTable>;
|
||||
|
||||
const currentUser: CurrentUser = {
|
||||
identity: 'admin',
|
||||
anonymous: false,
|
||||
provenancePermissions: {
|
||||
canRead: false,
|
||||
canWrite: false
|
||||
},
|
||||
countersPermissions: {
|
||||
canRead: false,
|
||||
canWrite: false
|
||||
},
|
||||
tenantsPermissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
controllerPermissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
policiesPermissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
systemPermissions: {
|
||||
canRead: true,
|
||||
canWrite: false
|
||||
},
|
||||
parameterContextPermissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
restrictedComponentsPermissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
},
|
||||
componentRestrictionPermissions: [
|
||||
{
|
||||
requiredPermission: {
|
||||
id: 'read-distributed-filesystem',
|
||||
label: 'read distributed filesystem'
|
||||
},
|
||||
permissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
}
|
||||
},
|
||||
{
|
||||
requiredPermission: {
|
||||
id: 'access-keytab',
|
||||
label: 'access keytab'
|
||||
},
|
||||
permissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
}
|
||||
},
|
||||
{
|
||||
requiredPermission: {
|
||||
id: 'export-nifi-details',
|
||||
label: 'export nifi details'
|
||||
},
|
||||
permissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
}
|
||||
},
|
||||
{
|
||||
requiredPermission: {
|
||||
id: 'read-filesystem',
|
||||
label: 'read filesystem'
|
||||
},
|
||||
permissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
}
|
||||
},
|
||||
{
|
||||
requiredPermission: {
|
||||
id: 'access-environment-credentials',
|
||||
label: 'access environment credentials'
|
||||
},
|
||||
permissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
}
|
||||
},
|
||||
{
|
||||
requiredPermission: {
|
||||
id: 'reference-remote-resources',
|
||||
label: 'reference remote resources'
|
||||
},
|
||||
permissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
}
|
||||
},
|
||||
{
|
||||
requiredPermission: {
|
||||
id: 'execute-code',
|
||||
label: 'execute code'
|
||||
},
|
||||
permissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
}
|
||||
},
|
||||
{
|
||||
requiredPermission: {
|
||||
id: 'access-ticket-cache',
|
||||
label: 'access ticket cache'
|
||||
},
|
||||
permissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
}
|
||||
},
|
||||
{
|
||||
requiredPermission: {
|
||||
id: 'write-filesystem',
|
||||
label: 'write filesystem'
|
||||
},
|
||||
permissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
}
|
||||
},
|
||||
{
|
||||
requiredPermission: {
|
||||
id: 'write-distributed-filesystem',
|
||||
label: 'write distributed filesystem'
|
||||
},
|
||||
permissions: {
|
||||
canRead: false,
|
||||
canWrite: true
|
||||
}
|
||||
}
|
||||
],
|
||||
canVersionFlows: false
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [UserTable],
|
||||
imports: [
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
MatSelectModule,
|
||||
NoopAnimationsModule
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(UserTable);
|
||||
component = fixture.componentInstance;
|
||||
component.currentUser = currentUser;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
* 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 { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Sort } from '@angular/material/sort';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||
import { CurrentUser } from '../../../../../state/current-user';
|
||||
import { AccessPolicySummaryEntity, UserEntity, UserGroupEntity } from '../../../../../state/shared';
|
||||
|
||||
export interface TenantItem {
|
||||
id: string;
|
||||
user: string;
|
||||
tenantType: 'user' | 'userGroup';
|
||||
membership: string[];
|
||||
configurable: boolean;
|
||||
}
|
||||
|
||||
export interface Tenants {
|
||||
users: UserEntity[];
|
||||
userGroups: UserGroupEntity[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'user-table',
|
||||
templateUrl: './user-table.component.html',
|
||||
styleUrls: ['./user-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
|
||||
})
|
||||
export class UserTable implements AfterViewInit {
|
||||
filterTerm: string = '';
|
||||
filterColumn: 'user' | 'membership' = 'user';
|
||||
totalCount: number = 0;
|
||||
filteredCount: number = 0;
|
||||
|
||||
displayedColumns: string[] = ['user', 'membership', 'actions'];
|
||||
dataSource: MatTableDataSource<TenantItem> = new MatTableDataSource<TenantItem>();
|
||||
filterForm: FormGroup;
|
||||
|
||||
userLookup: Map<string, UserEntity> = new Map<string, UserEntity>();
|
||||
userGroupLookup: Map<string, UserGroupEntity> = new Map<string, UserGroupEntity>();
|
||||
|
||||
@Input() set tenants(tenants: Tenants) {
|
||||
this.userLookup.clear();
|
||||
this.userGroupLookup.clear();
|
||||
|
||||
const tenantItems: TenantItem[] = [];
|
||||
tenants.users.forEach((user) => {
|
||||
this.userLookup.set(user.id, user);
|
||||
tenantItems.push({
|
||||
id: user.id,
|
||||
tenantType: 'user',
|
||||
user: user.component.identity,
|
||||
membership: user.component.userGroups.map((userGroup) => userGroup.component.identity),
|
||||
configurable: user.component.configurable
|
||||
});
|
||||
});
|
||||
tenants.userGroups.forEach((userGroup) => {
|
||||
this.userGroupLookup.set(userGroup.id, userGroup);
|
||||
tenantItems.push({
|
||||
id: userGroup.id,
|
||||
tenantType: 'userGroup',
|
||||
user: userGroup.component.identity,
|
||||
membership: userGroup.component.users.map((user) => user.component.identity),
|
||||
configurable: userGroup.component.configurable
|
||||
});
|
||||
});
|
||||
|
||||
this.dataSource.data = this.sortUsers(tenantItems, this.sort);
|
||||
this.dataSource.filterPredicate = (data: TenantItem, filter: string) => {
|
||||
const { filterTerm, filterColumn } = JSON.parse(filter);
|
||||
if (filterColumn === 'user') {
|
||||
return this.nifiCommon.stringContains(data.user, filterTerm, true);
|
||||
} else {
|
||||
return this.nifiCommon.stringContains(this.formatMembership(data), filterTerm, true);
|
||||
}
|
||||
};
|
||||
this.totalCount = tenantItems.length;
|
||||
this.filteredCount = tenantItems.length;
|
||||
|
||||
// apply any filtering to the new data
|
||||
const filterTerm = this.filterForm.get('filterTerm')?.value;
|
||||
if (filterTerm?.length > 0) {
|
||||
const filterColumn = this.filterForm.get('filterColumn')?.value;
|
||||
this.applyFilter(filterTerm, filterColumn);
|
||||
}
|
||||
}
|
||||
|
||||
@Input() selectedTenantId!: string;
|
||||
@Input() currentUser!: CurrentUser;
|
||||
@Input() configurableUsersAndGroups!: boolean;
|
||||
|
||||
@Output() createTenant: EventEmitter<void> = new EventEmitter<void>();
|
||||
@Output() selectTenant: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Output() editTenant: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Output() deleteUser: EventEmitter<UserEntity> = new EventEmitter<UserEntity>();
|
||||
@Output() deleteUserGroup: EventEmitter<UserGroupEntity> = new EventEmitter<UserGroupEntity>();
|
||||
@Output() viewAccessPolicies: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
sort: Sort = {
|
||||
active: 'user',
|
||||
direction: 'asc'
|
||||
};
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {
|
||||
this.filterForm = this.formBuilder.group({ filterTerm: '', filterColumn: 'user' });
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.filterForm
|
||||
.get('filterTerm')
|
||||
?.valueChanges.pipe(debounceTime(500))
|
||||
.subscribe((filterTerm: string) => {
|
||||
const filterColumn = this.filterForm.get('filterColumn')?.value;
|
||||
this.applyFilter(filterTerm, filterColumn);
|
||||
});
|
||||
|
||||
this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
|
||||
const filterTerm = this.filterForm.get('filterTerm')?.value;
|
||||
this.applyFilter(filterTerm, filterColumn);
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter(filterTerm: string, filterColumn: string) {
|
||||
this.dataSource.filter = JSON.stringify({ filterTerm, filterColumn });
|
||||
this.filteredCount = this.dataSource.filteredData.length;
|
||||
}
|
||||
|
||||
updateSort(sort: Sort): void {
|
||||
this.sort = sort;
|
||||
this.dataSource.data = this.sortUsers(this.dataSource.data, sort);
|
||||
}
|
||||
|
||||
formatMembership(item: TenantItem): string {
|
||||
return item.membership.sort((a, b) => this.nifiCommon.compareString(a, b)).join(', ');
|
||||
}
|
||||
|
||||
sortUsers(items: TenantItem[], sort: Sort): TenantItem[] {
|
||||
const data: TenantItem[] = items.slice();
|
||||
return data.sort((a, b) => {
|
||||
const isAsc = sort.direction === 'asc';
|
||||
|
||||
let retVal: number = 0;
|
||||
switch (sort.active) {
|
||||
case 'user':
|
||||
retVal = this.nifiCommon.compareString(a.user, b.user);
|
||||
break;
|
||||
case 'membership':
|
||||
retVal = this.nifiCommon.compareString(this.formatMembership(a), this.formatMembership(b));
|
||||
break;
|
||||
}
|
||||
|
||||
return retVal * (isAsc ? 1 : -1);
|
||||
});
|
||||
}
|
||||
|
||||
select(item: TenantItem): void {
|
||||
this.selectTenant.next(item.id);
|
||||
}
|
||||
|
||||
isSelected(item: TenantItem): boolean {
|
||||
if (this.selectedTenantId) {
|
||||
return item.id == this.selectedTenantId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
canModifyTenants(currentUser: CurrentUser): boolean {
|
||||
return (
|
||||
currentUser.tenantsPermissions.canRead &&
|
||||
currentUser.tenantsPermissions.canWrite &&
|
||||
this.configurableUsersAndGroups
|
||||
);
|
||||
}
|
||||
|
||||
createClicked(): void {
|
||||
this.createTenant.next();
|
||||
}
|
||||
|
||||
canEditOrDelete(currentUser: CurrentUser, item: TenantItem): boolean {
|
||||
return this.canModifyTenants(currentUser) && item.configurable;
|
||||
}
|
||||
|
||||
editClicked(item: TenantItem, event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
this.editTenant.next(item.id);
|
||||
}
|
||||
|
||||
deleteClicked(item: TenantItem): void {
|
||||
if (item.tenantType === 'user') {
|
||||
const user: UserEntity | undefined = this.userLookup.get(item.id);
|
||||
if (user) {
|
||||
this.deleteUser.next(user);
|
||||
}
|
||||
} else if (item.tenantType === 'userGroup') {
|
||||
const userGroup: UserGroupEntity | undefined = this.userGroupLookup.get(item.id);
|
||||
if (userGroup) {
|
||||
this.deleteUserGroup.next(userGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getAccessPolicies(item: TenantItem): AccessPolicySummaryEntity[] {
|
||||
const accessPolicies: AccessPolicySummaryEntity[] = [];
|
||||
if (item.tenantType === 'user') {
|
||||
const user: UserEntity | undefined = this.userLookup.get(item.id);
|
||||
if (user) {
|
||||
accessPolicies.push(...user.component.accessPolicies);
|
||||
}
|
||||
} else if (item.tenantType === 'userGroup') {
|
||||
const userGroup: UserGroupEntity | undefined = this.userGroupLookup.get(item.id);
|
||||
if (userGroup) {
|
||||
accessPolicies.push(...userGroup.component.accessPolicies);
|
||||
}
|
||||
}
|
||||
return accessPolicies;
|
||||
}
|
||||
|
||||
hasAccessPolicies(item: TenantItem): boolean {
|
||||
return !this.nifiCommon.isEmpty(this.getAccessPolicies(item));
|
||||
}
|
||||
|
||||
viewAccessPoliciesClicked(item: TenantItem, event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
this.viewAccessPolicies.next(item.id);
|
||||
}
|
||||
}
|
|
@ -20,12 +20,12 @@ import { Observable, throwError } from 'rxjs';
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UserService {
|
||||
export class CurrentUserService {
|
||||
private static readonly API: string = '../nifi-api';
|
||||
|
||||
constructor(private httpClient: HttpClient) {}
|
||||
|
||||
getUser(): Observable<any> {
|
||||
return this.httpClient.get(`${UserService.API}/flow/current-user`);
|
||||
return this.httpClient.get(`${CurrentUserService.API}/flow/current-user`);
|
||||
}
|
||||
}
|
|
@ -20,17 +20,17 @@ import { inject } from '@angular/core';
|
|||
import { AuthService } from '../auth.service';
|
||||
import { AuthStorage } from '../auth-storage.service';
|
||||
import { take } from 'rxjs';
|
||||
import { UserService } from '../user.service';
|
||||
import { CurrentUserService } from '../current-user.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { UserState } from '../../state/user';
|
||||
import { loadUserSuccess } from '../../state/user/user.actions';
|
||||
import { selectUserState } from '../../state/user/user.selectors';
|
||||
import { CurrentUserState } from '../../state/current-user';
|
||||
import { loadCurrentUserSuccess } from '../../state/current-user/current-user.actions';
|
||||
import { selectCurrentUserState } from '../../state/current-user/current-user.selectors';
|
||||
|
||||
export const authenticationGuard: CanMatchFn = (route, state) => {
|
||||
const authStorage: AuthStorage = inject(AuthStorage);
|
||||
const authService: AuthService = inject(AuthService);
|
||||
const userService: UserService = inject(UserService);
|
||||
const store: Store<UserState> = inject(Store<UserState>);
|
||||
const userService: CurrentUserService = inject(CurrentUserService);
|
||||
const store: Store<CurrentUserState> = inject(Store<CurrentUserState>);
|
||||
|
||||
const handleAuthentication: Promise<boolean> = new Promise((resolve) => {
|
||||
if (authStorage.hasToken()) {
|
||||
|
@ -77,7 +77,7 @@ export const authenticationGuard: CanMatchFn = (route, state) => {
|
|||
return new Promise<boolean>((resolve) => {
|
||||
handleAuthentication.finally(() => {
|
||||
store
|
||||
.select(selectUserState)
|
||||
.select(selectCurrentUserState)
|
||||
.pipe(take(1))
|
||||
.subscribe((userState) => {
|
||||
if (userState.status == 'pending') {
|
||||
|
@ -88,7 +88,7 @@ export const authenticationGuard: CanMatchFn = (route, state) => {
|
|||
next: (response) => {
|
||||
// store the loaded user
|
||||
store.dispatch(
|
||||
loadUserSuccess({
|
||||
loadCurrentUserSuccess({
|
||||
response: {
|
||||
user: response
|
||||
}
|
||||
|
|
|
@ -19,15 +19,15 @@ 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';
|
||||
import { CurrentUser, CurrentUserState } from '../../state/current-user';
|
||||
import { selectCurrentUser } from '../../state/current-user/current-user.selectors';
|
||||
|
||||
export const authorizationGuard = (authorizationCheck: (user: User) => boolean): CanMatchFn => {
|
||||
export const authorizationGuard = (authorizationCheck: (user: CurrentUser) => boolean): CanMatchFn => {
|
||||
return (route: Route, state: UrlSegment[]) => {
|
||||
const router: Router = inject(Router);
|
||||
const store: Store<UserState> = inject(Store<UserState>);
|
||||
const store: Store<CurrentUserState> = inject(Store<CurrentUserState>);
|
||||
|
||||
return store.select(selectUser).pipe(
|
||||
return store.select(selectCurrentUser).pipe(
|
||||
map((currentUser) => {
|
||||
if (authorizationCheck(currentUser)) {
|
||||
return true;
|
||||
|
|
|
@ -19,7 +19,7 @@ import { TestBed } from '@angular/core/testing';
|
|||
|
||||
import { PollingInterceptor } from './polling.interceptor';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../state/user/user.reducer';
|
||||
import { initialState } from '../../state/current-user/current-user.reducer';
|
||||
|
||||
describe('PollingInterceptor', () => {
|
||||
let service: PollingInterceptor;
|
||||
|
|
|
@ -20,7 +20,7 @@ import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest
|
|||
import { Observable, tap } from 'rxjs';
|
||||
import { NiFiState } from '../../state';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { stopUserPolling } from '../../state/user/user.actions';
|
||||
import { stopCurrentUserPolling } from '../../state/current-user/current-user.actions';
|
||||
import { stopProcessGroupPolling } from '../../pages/flow-designer/state/flow/flow.actions';
|
||||
|
||||
@Injectable({
|
||||
|
@ -34,7 +34,7 @@ export class PollingInterceptor implements HttpInterceptor {
|
|||
tap({
|
||||
error: (error) => {
|
||||
if (error instanceof HttpErrorResponse && error.status === 0) {
|
||||
this.store.dispatch(stopUserPolling());
|
||||
this.store.dispatch(stopCurrentUserPolling());
|
||||
this.store.dispatch(stopProcessGroupPolling());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SelectOption } from '../state/shared';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -37,6 +38,65 @@ export class NiFiCommon {
|
|||
public static readonly BYTES_IN_GIGABYTE: number = 1073741824;
|
||||
public static readonly BYTES_IN_TERABYTE: number = 1099511627776;
|
||||
|
||||
private policyTypeListing: SelectOption[] = [
|
||||
{
|
||||
text: 'view the user interface',
|
||||
value: 'flow',
|
||||
description: 'Allows users to view the user interface'
|
||||
},
|
||||
{
|
||||
text: 'access the controller',
|
||||
value: 'controller',
|
||||
description:
|
||||
'Allows users to view/modify the controller including Management Controller Services, Reporting Tasks, Registry Clients, Parameter Providers and nodes in the cluster'
|
||||
},
|
||||
{
|
||||
text: 'access parameter contexts',
|
||||
value: 'parameter-contexts',
|
||||
description: 'Allows users to view/modify Parameter Contexts'
|
||||
},
|
||||
{
|
||||
text: 'query provenance',
|
||||
value: 'provenance',
|
||||
description: 'Allows users to submit a Provenance Search and request Event Lineage'
|
||||
},
|
||||
{
|
||||
text: 'access restricted components',
|
||||
value: 'restricted-components',
|
||||
description: 'Allows users to create/modify restricted components assuming other permissions are sufficient'
|
||||
},
|
||||
{
|
||||
text: 'access all policies',
|
||||
value: 'policies',
|
||||
description: 'Allows users to view/modify the policies for all components'
|
||||
},
|
||||
{
|
||||
text: 'access users/user groups',
|
||||
value: 'tenants',
|
||||
description: 'Allows users to view/modify the users and user groups'
|
||||
},
|
||||
{
|
||||
text: 'retrieve site-to-site details',
|
||||
value: 'site-to-site',
|
||||
description: 'Allows other NiFi instances to retrieve Site-To-Site details of this NiFi'
|
||||
},
|
||||
{
|
||||
text: 'view system diagnostics',
|
||||
value: 'system',
|
||||
description: 'Allows users to view System Diagnostics'
|
||||
},
|
||||
{
|
||||
text: 'proxy user requests',
|
||||
value: 'proxy',
|
||||
description: 'Allows proxy machines to send requests on the behalf of others'
|
||||
},
|
||||
{
|
||||
text: 'access counters',
|
||||
value: 'counters',
|
||||
description: 'Allows users to view/modify Counters'
|
||||
}
|
||||
];
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
|
@ -435,4 +495,13 @@ export class NiFiCommon {
|
|||
const locale: string = (navigator && navigator.language) || 'en';
|
||||
return f.toLocaleString(locale, { maximumFractionDigits: 2, minimumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the policy type for the specified resource.
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
public getPolicyTypeListing(value: string): SelectOption | undefined {
|
||||
return this.policyTypeListing.find((policy: SelectOption) => value === policy.value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,17 +16,19 @@
|
|||
*/
|
||||
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { LoadUserResponse, UserState } from './index';
|
||||
import { LoadProcessGroupRequest, LoadProcessGroupResponse } from '../../pages/flow-designer/state/flow';
|
||||
import { LoadCurrentUserResponse } from './index';
|
||||
|
||||
export const loadUser = createAction('[User] Load User');
|
||||
export const loadCurrentUser = createAction('[Current User] Load Current User');
|
||||
|
||||
export const loadUserSuccess = createAction('[User] Load User Success', props<{ response: LoadUserResponse }>());
|
||||
export const loadCurrentUserSuccess = createAction(
|
||||
'[Current User] Load Current User Success',
|
||||
props<{ response: LoadCurrentUserResponse }>()
|
||||
);
|
||||
|
||||
export const userApiError = createAction('[User] User Api Error', props<{ error: string }>());
|
||||
export const currentUserApiError = createAction('[Current User] Current User Api Error', props<{ error: string }>());
|
||||
|
||||
export const clearUserApiError = createAction('[User] Clear User Api Error');
|
||||
export const clearCurrentUserApiError = createAction('[Current User] Clear Current User Api Error');
|
||||
|
||||
export const startUserPolling = createAction('[User] Start User Polling');
|
||||
export const startCurrentUserPolling = createAction('[Current User] Start Current User Polling');
|
||||
|
||||
export const stopUserPolling = createAction('[User] Stop User Polling');
|
||||
export const stopCurrentUserPolling = createAction('[Current User] Stop Current User Polling');
|
|
@ -17,44 +17,46 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
||||
import * as UserActions from './user.actions';
|
||||
import * as UserActions from './current-user.actions';
|
||||
import { asyncScheduler, catchError, from, interval, map, of, switchMap, takeUntil } from 'rxjs';
|
||||
import { UserService } from '../../service/user.service';
|
||||
import { CurrentUserService } from '../../service/current-user.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserEffects {
|
||||
export class CurrentUserEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private userService: UserService
|
||||
private userService: CurrentUserService
|
||||
) {}
|
||||
|
||||
loadUser$ = createEffect(() =>
|
||||
loadCurrentUser$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserActions.loadUser),
|
||||
ofType(UserActions.loadCurrentUser),
|
||||
switchMap(() => {
|
||||
return from(
|
||||
this.userService.getUser().pipe(
|
||||
map((response) =>
|
||||
UserActions.loadUserSuccess({
|
||||
UserActions.loadCurrentUserSuccess({
|
||||
response: {
|
||||
user: response
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) => of(UserActions.userApiError({ error: error.error })))
|
||||
catchError((error) => of(UserActions.currentUserApiError({ error: error.error })))
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
startUserPolling$ = createEffect(() =>
|
||||
startCurrentUserPolling$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(UserActions.startUserPolling),
|
||||
ofType(UserActions.startCurrentUserPolling),
|
||||
switchMap(() =>
|
||||
interval(30000, asyncScheduler).pipe(takeUntil(this.actions$.pipe(ofType(UserActions.stopUserPolling))))
|
||||
interval(30000, asyncScheduler).pipe(
|
||||
takeUntil(this.actions$.pipe(ofType(UserActions.stopCurrentUserPolling)))
|
||||
)
|
||||
),
|
||||
switchMap(() => of(UserActions.loadUser()))
|
||||
switchMap(() => of(UserActions.loadCurrentUser()))
|
||||
)
|
||||
);
|
||||
}
|
|
@ -16,16 +16,21 @@
|
|||
*/
|
||||
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import { UserState } from './index';
|
||||
import { CurrentUserState } from './index';
|
||||
import { Permissions } from '../shared';
|
||||
import { clearUserApiError, loadUser, loadUserSuccess, userApiError } from './user.actions';
|
||||
import {
|
||||
clearCurrentUserApiError,
|
||||
loadCurrentUser,
|
||||
loadCurrentUserSuccess,
|
||||
currentUserApiError
|
||||
} from './current-user.actions';
|
||||
|
||||
export const NO_PERMISSIONS: Permissions = {
|
||||
canRead: false,
|
||||
canWrite: false
|
||||
};
|
||||
|
||||
export const initialState: UserState = {
|
||||
export const initialState: CurrentUserState = {
|
||||
user: {
|
||||
identity: '',
|
||||
anonymous: true,
|
||||
|
@ -44,24 +49,24 @@ export const initialState: UserState = {
|
|||
status: 'pending'
|
||||
};
|
||||
|
||||
export const userReducer = createReducer(
|
||||
export const currentUserReducer = createReducer(
|
||||
initialState,
|
||||
on(loadUser, (state) => ({
|
||||
on(loadCurrentUser, (state) => ({
|
||||
...state,
|
||||
status: 'loading' as const
|
||||
})),
|
||||
on(loadUserSuccess, (state, { response }) => ({
|
||||
on(loadCurrentUserSuccess, (state, { response }) => ({
|
||||
...state,
|
||||
user: response.user,
|
||||
error: null,
|
||||
status: 'success' as const
|
||||
})),
|
||||
on(userApiError, (state, { error }) => ({
|
||||
on(currentUserApiError, (state, { error }) => ({
|
||||
...state,
|
||||
error: error,
|
||||
status: 'error' as const
|
||||
})),
|
||||
on(clearUserApiError, (state) => ({
|
||||
on(clearCurrentUserApiError, (state) => ({
|
||||
...state,
|
||||
error: null,
|
||||
status: 'pending' as const
|
|
@ -16,8 +16,8 @@
|
|||
*/
|
||||
|
||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { userFeatureKey, UserState } from './index';
|
||||
import { currentUserFeatureKey, CurrentUserState } from './index';
|
||||
|
||||
export const selectUserState = createFeatureSelector<UserState>(userFeatureKey);
|
||||
export const selectCurrentUserState = createFeatureSelector<CurrentUserState>(currentUserFeatureKey);
|
||||
|
||||
export const selectUser = createSelector(selectUserState, (state: UserState) => state.user);
|
||||
export const selectCurrentUser = createSelector(selectCurrentUserState, (state: CurrentUserState) => state.user);
|
|
@ -17,10 +17,10 @@
|
|||
|
||||
import { Permissions, RequiredPermission } from '../shared';
|
||||
|
||||
export const userFeatureKey = 'user';
|
||||
export const currentUserFeatureKey = 'currentUser';
|
||||
|
||||
export interface LoadUserResponse {
|
||||
user: User;
|
||||
export interface LoadCurrentUserResponse {
|
||||
user: CurrentUser;
|
||||
}
|
||||
|
||||
export interface ComponentRestrictionPermission {
|
||||
|
@ -28,7 +28,7 @@ export interface ComponentRestrictionPermission {
|
|||
permissions: Permissions;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
export interface CurrentUser {
|
||||
identity: string;
|
||||
anonymous: boolean;
|
||||
canVersionFlows: boolean;
|
||||
|
@ -43,8 +43,8 @@ export interface User {
|
|||
componentRestrictionPermissions: ComponentRestrictionPermission[];
|
||||
}
|
||||
|
||||
export interface UserState {
|
||||
user: User;
|
||||
export interface CurrentUserState {
|
||||
user: CurrentUser;
|
||||
error: string | null;
|
||||
status: 'pending' | 'loading' | 'error' | 'success';
|
||||
}
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
import { routerReducer, RouterReducerState } from '@ngrx/router-store';
|
||||
import { ActionReducerMap } from '@ngrx/store';
|
||||
import { UserState, userFeatureKey } from './user';
|
||||
import { userReducer } from './user/user.reducer';
|
||||
import { CurrentUserState, currentUserFeatureKey } from './current-user';
|
||||
import { currentUserReducer } from './current-user/current-user.reducer';
|
||||
import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
|
||||
import { extensionTypesReducer } from './extension-types/extension-types.reducer';
|
||||
import { aboutFeatureKey, AboutState } from './about';
|
||||
|
@ -32,7 +32,7 @@ import { systemDiagnosticsReducer } from './system-diagnostics/system-diagnostic
|
|||
|
||||
export interface NiFiState {
|
||||
router: RouterReducerState;
|
||||
[userFeatureKey]: UserState;
|
||||
[currentUserFeatureKey]: CurrentUserState;
|
||||
[extensionTypesFeatureKey]: ExtensionTypesState;
|
||||
[aboutFeatureKey]: AboutState;
|
||||
[statusHistoryFeatureKey]: StatusHistoryState;
|
||||
|
@ -42,7 +42,7 @@ export interface NiFiState {
|
|||
|
||||
export const rootReducers: ActionReducerMap<NiFiState> = {
|
||||
router: routerReducer,
|
||||
[userFeatureKey]: userReducer,
|
||||
[currentUserFeatureKey]: currentUserReducer,
|
||||
[extensionTypesFeatureKey]: extensionTypesReducer,
|
||||
[aboutFeatureKey]: aboutReducer,
|
||||
[statusHistoryFeatureKey]: statusHistoryReducer,
|
||||
|
|
|
@ -49,6 +49,58 @@ export interface EditParameterResponse {
|
|||
parameter: Parameter;
|
||||
}
|
||||
|
||||
export interface UserEntity {
|
||||
id: string;
|
||||
permissions: Permissions;
|
||||
component: User;
|
||||
revision: Revision;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export interface User extends Tenant {
|
||||
userGroups: TenantEntity[];
|
||||
accessPolicies: AccessPolicySummaryEntity[];
|
||||
}
|
||||
|
||||
export interface UserGroupEntity {
|
||||
id: string;
|
||||
permissions: Permissions;
|
||||
component: UserGroup;
|
||||
revision: Revision;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
export interface UserGroup extends Tenant {
|
||||
users: TenantEntity[];
|
||||
accessPolicies: AccessPolicySummaryEntity[];
|
||||
}
|
||||
|
||||
export interface TenantEntity {
|
||||
id: string;
|
||||
revision: Revision;
|
||||
permissions: Permissions;
|
||||
component: Tenant;
|
||||
}
|
||||
|
||||
export interface Tenant {
|
||||
id: string;
|
||||
identity: string;
|
||||
configurable: boolean;
|
||||
}
|
||||
|
||||
export interface EditTenantRequest {
|
||||
user?: UserEntity;
|
||||
userGroup?: UserGroupEntity;
|
||||
existingUsers: UserEntity[];
|
||||
existingUserGroups: UserGroupEntity[];
|
||||
}
|
||||
|
||||
export interface EditTenantResponse {
|
||||
revision: Revision;
|
||||
user?: any;
|
||||
userGroup?: any;
|
||||
}
|
||||
|
||||
export interface CreateControllerServiceRequest {
|
||||
controllerServiceTypes: DocumentedType[];
|
||||
}
|
||||
|
@ -352,6 +404,35 @@ export interface ControllerServiceEntity {
|
|||
component: any;
|
||||
}
|
||||
|
||||
export interface AccessPolicySummaryEntity {
|
||||
id: string;
|
||||
component: AccessPolicySummary;
|
||||
revision: Revision;
|
||||
permissions: Permissions;
|
||||
}
|
||||
|
||||
export interface AccessPolicySummary {
|
||||
id: string;
|
||||
resource: string;
|
||||
action: string;
|
||||
componentReference?: ComponentReferenceEntity;
|
||||
configurable: boolean;
|
||||
}
|
||||
|
||||
export interface ComponentReferenceEntity {
|
||||
id: string;
|
||||
parentGroupId?: string;
|
||||
component: ComponentReference;
|
||||
revision: Revision;
|
||||
permissions: Permissions;
|
||||
}
|
||||
|
||||
export interface ComponentReference {
|
||||
id: string;
|
||||
parentGroupId?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DocumentedType {
|
||||
bundle: Bundle;
|
||||
description?: string;
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<!--
|
||||
~ 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>{{ isNew ? 'Add' : 'Edit' }} {{ isUser ? 'User' : 'User Group' }}</h2>
|
||||
<form class="edit-tenant-form" [formGroup]="editTenantForm">
|
||||
<mat-dialog-content>
|
||||
<div class="mb-6">
|
||||
<mat-radio-group formControlName="tenantType" (change)="tenantTypeChanged()">
|
||||
<mat-radio-button [value]="USER">Individual</mat-radio-button>
|
||||
<mat-radio-button [value]="USER_GROUP">Group</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<mat-form-field>
|
||||
<mat-label>Identity</mat-label>
|
||||
<input matInput formControlName="identity" type="text" />
|
||||
<mat-error *ngIf="identity.invalid">{{ getIdentityErrorMessage() }}</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="isUser; else isUserGroup">
|
||||
<mat-label>Member of</mat-label>
|
||||
<mat-selection-list formControlName="userGroups">
|
||||
<mat-list-option *ngFor="let userGroup of userGroups" togglePosition="before" [value]="userGroup.id"
|
||||
>{{ userGroup.component.identity }}
|
||||
</mat-list-option>
|
||||
</mat-selection-list>
|
||||
</div>
|
||||
<ng-template #isUserGroup>
|
||||
<div>
|
||||
<mat-label>Members</mat-label>
|
||||
<mat-selection-list formControlName="users">
|
||||
<mat-list-option *ngFor="let user of users" togglePosition="before" [value]="user.id"
|
||||
>{{ user.component.identity }}
|
||||
</mat-list-option>
|
||||
</mat-selection-list>
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end" *ngIf="{ value: (saving$ | async)! } as saving">
|
||||
<button mat-raised-button mat-dialog-close color="accent">Cancel</button>
|
||||
<button
|
||||
mat-raised-button
|
||||
[disabled]="editTenantForm.invalid || saving.value"
|
||||
(click)="okClicked()"
|
||||
color="primary">
|
||||
<span *nifiSpinner="saving.value">Ok</span>
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.edit-tenant-form {
|
||||
@include mat.button-density(-1);
|
||||
|
||||
.mat-mdc-radio-button ~ .mat-mdc-radio-button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.mat-mdc-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mat-mdc-form-field-error {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
mat-selection-list {
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,798 @@
|
|||
/*
|
||||
* 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 { EditTenantDialog } from './edit-tenant-dialog.component';
|
||||
import { EditTenantRequest } from '../../../state/shared';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('EditTenantDialog', () => {
|
||||
let component: EditTenantDialog;
|
||||
let fixture: ComponentFixture<EditTenantDialog>;
|
||||
|
||||
const data: EditTenantRequest = {
|
||||
user: {
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfc1479-018c-1000-1025-6cc5b4adefb8',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/acfc1479-018c-1000-1025-6cc5b4adefb8',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfc1479-018c-1000-1025-6cc5b4adefb8',
|
||||
identity: 'Group 2',
|
||||
configurable: true,
|
||||
userGroups: [],
|
||||
accessPolicies: []
|
||||
}
|
||||
},
|
||||
existingUsers: [
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'ad0ddd93-018c-1000-4e40-8e3b207abdcd',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/ad0ddd93-018c-1000-4e40-8e3b207abdcd',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'ad0ddd93-018c-1000-4e40-8e3b207abdcd',
|
||||
identity: 'user 1',
|
||||
configurable: true,
|
||||
userGroups: [
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfbfa2c-018c-1000-0311-47b83e34c9c3',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfbfa2c-018c-1000-0311-47b83e34c9c3',
|
||||
identity: 'group 1',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
],
|
||||
accessPolicies: [
|
||||
{
|
||||
revision: {
|
||||
clientId: 'b09bd713-018c-1000-e5b8-14855e466f1b',
|
||||
version: 4
|
||||
},
|
||||
id: 'b0c3148d-018c-1000-2cfe-8fab902c11f7',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'b0c3148d-018c-1000-2cfe-8fab902c11f7',
|
||||
resource: '/system',
|
||||
action: 'read',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'ad0875a3-018c-1000-1877-3f4ebf93f91e',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/ad0875a3-018c-1000-1877-3f4ebf93f91e',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'ad0875a3-018c-1000-1877-3f4ebf93f91e',
|
||||
identity: 'user 2',
|
||||
configurable: true,
|
||||
userGroups: [
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfbfa2c-018c-1000-0311-47b83e34c9c3',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfbfa2c-018c-1000-0311-47b83e34c9c3',
|
||||
identity: 'group 1',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfcdcf6-018c-1000-604f-84da632fdbd5',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfcdcf6-018c-1000-604f-84da632fdbd5',
|
||||
identity: 'group 9',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'a69482a2-018c-1000-2c02-fac31f4b102b',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'a69482a2-018c-1000-2c02-fac31f4b102b',
|
||||
identity: 'Group',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
],
|
||||
accessPolicies: [
|
||||
{
|
||||
revision: {
|
||||
clientId: 'b09bd713-018c-1000-e5b8-14855e466f1b',
|
||||
version: 4
|
||||
},
|
||||
id: 'b0c3148d-018c-1000-2cfe-8fab902c11f7',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'b0c3148d-018c-1000-2cfe-8fab902c11f7',
|
||||
resource: '/system',
|
||||
action: 'read',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfc1479-018c-1000-1025-6cc5b4adefb8',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/acfc1479-018c-1000-1025-6cc5b4adefb8',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfc1479-018c-1000-1025-6cc5b4adefb8',
|
||||
identity: 'Group 2',
|
||||
configurable: true,
|
||||
userGroups: [],
|
||||
accessPolicies: []
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfc879d-018c-1000-c93e-21350df0f5bf',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/acfc879d-018c-1000-c93e-21350df0f5bf',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfc879d-018c-1000-c93e-21350df0f5bf',
|
||||
identity: 'group 7',
|
||||
configurable: true,
|
||||
userGroups: [],
|
||||
accessPolicies: []
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: '21232f29-7a57-35a7-8389-4a0e4a801fc3',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/21232f29-7a57-35a7-8389-4a0e4a801fc3',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: '21232f29-7a57-35a7-8389-4a0e4a801fc3',
|
||||
identity: 'admin',
|
||||
configurable: true,
|
||||
userGroups: [
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'a69482a2-018c-1000-2c02-fac31f4b102b',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'a69482a2-018c-1000-2c02-fac31f4b102b',
|
||||
identity: 'Group',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
],
|
||||
accessPolicies: [
|
||||
{
|
||||
revision: {
|
||||
clientId: 'b09bd713-018c-1000-e5b8-14855e466f1b',
|
||||
version: 4
|
||||
},
|
||||
id: 'b0c3148d-018c-1000-2cfe-8fab902c11f7',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'b0c3148d-018c-1000-2cfe-8fab902c11f7',
|
||||
resource: '/system',
|
||||
action: 'read',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: '15e4e0bd-cb28-34fd-8587-f8d15162cba5',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: '15e4e0bd-cb28-34fd-8587-f8d15162cba5',
|
||||
resource: '/tenants',
|
||||
action: 'write',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'c6322e6c-4cc1-3bcc-91b3-2ed2111674cf',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'c6322e6c-4cc1-3bcc-91b3-2ed2111674cf',
|
||||
resource: '/controller',
|
||||
action: 'write',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'f99bccd1-a30e-3e4a-98a2-dbc708edc67f',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'f99bccd1-a30e-3e4a-98a2-dbc708edc67f',
|
||||
resource: '/flow',
|
||||
action: 'read',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: '627410be-1717-35b4-a06f-e9362b89e0b7',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: '627410be-1717-35b4-a06f-e9362b89e0b7',
|
||||
resource: '/tenants',
|
||||
action: 'read',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: '2e1015cb-0fed-3005-8e0d-722311f21a03',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: '2e1015cb-0fed-3005-8e0d-722311f21a03',
|
||||
resource: '/controller',
|
||||
action: 'read',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'b8775bd4-704a-34c6-987b-84f2daf7a515',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'b8775bd4-704a-34c6-987b-84f2daf7a515',
|
||||
resource: '/restricted-components',
|
||||
action: 'write',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: '92db2c23-018c-1000-8885-b650a16dcb32',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: '92db2c23-018c-1000-8885-b650a16dcb32',
|
||||
resource: '/process-groups/92dade11-018c-1000-91a9-ad537020d5cb',
|
||||
action: 'read',
|
||||
componentReference: {
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: '92dade11-018c-1000-91a9-ad537020d5cb',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: '92dade11-018c-1000-91a9-ad537020d5cb',
|
||||
name: 'NiFi Flow'
|
||||
}
|
||||
},
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: '92db4367-018c-1000-e220-2dbec57c389b',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: '92db4367-018c-1000-e220-2dbec57c389b',
|
||||
resource: '/process-groups/92dade11-018c-1000-91a9-ad537020d5cb',
|
||||
action: 'write',
|
||||
componentReference: {
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: '92dade11-018c-1000-91a9-ad537020d5cb',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: '92dade11-018c-1000-91a9-ad537020d5cb',
|
||||
name: 'NiFi Flow'
|
||||
}
|
||||
},
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'ad99ea98-3af6-3561-ae27-5bf09e1d969d',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'ad99ea98-3af6-3561-ae27-5bf09e1d969d',
|
||||
resource: '/policies',
|
||||
action: 'write',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'ff96062a-fa99-36dc-9942-0f6442ae7212',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'ff96062a-fa99-36dc-9942-0f6442ae7212',
|
||||
resource: '/policies',
|
||||
action: 'read',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'b1d4bb80-018c-1000-6cb7-a1e977f8382b',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'b1d4bb80-018c-1000-6cb7-a1e977f8382b',
|
||||
resource: '/processors/ad55f343-018c-1000-1291-1992288346e5',
|
||||
action: 'read',
|
||||
componentReference: {
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'ad55f343-018c-1000-1291-1992288346e5',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
parentGroupId: '92dade11-018c-1000-91a9-ad537020d5cb',
|
||||
component: {
|
||||
id: 'ad55f343-018c-1000-1291-1992288346e5',
|
||||
parentGroupId: '92dade11-018c-1000-91a9-ad537020d5cb',
|
||||
name: 'InvokeHTTP'
|
||||
}
|
||||
},
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'abf8bb43-018c-1000-3e0c-a4405f39c934',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/abf8bb43-018c-1000-3e0c-a4405f39c934',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'abf8bb43-018c-1000-3e0c-a4405f39c934',
|
||||
identity: 'test',
|
||||
configurable: true,
|
||||
userGroups: [
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'a69482a2-018c-1000-2c02-fac31f4b102b',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'a69482a2-018c-1000-2c02-fac31f4b102b',
|
||||
identity: 'Group',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
],
|
||||
accessPolicies: []
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfc259e-018c-1000-2e1e-01919d4b0546',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/acfc259e-018c-1000-2e1e-01919d4b0546',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfc259e-018c-1000-2e1e-01919d4b0546',
|
||||
identity: 'group 3',
|
||||
configurable: true,
|
||||
userGroups: [],
|
||||
accessPolicies: []
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfca1a8-018c-1000-6852-0dd013e20ee7',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/acfca1a8-018c-1000-6852-0dd013e20ee7',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfca1a8-018c-1000-6852-0dd013e20ee7',
|
||||
identity: 'group 8',
|
||||
configurable: true,
|
||||
userGroups: [],
|
||||
accessPolicies: []
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfc5a70-018c-1000-a702-b52236aaacea',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/acfc5a70-018c-1000-a702-b52236aaacea',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfc5a70-018c-1000-a702-b52236aaacea',
|
||||
identity: 'group 5',
|
||||
configurable: true,
|
||||
userGroups: [],
|
||||
accessPolicies: []
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfc6ee7-018c-1000-feed-fb1b89482663',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/acfc6ee7-018c-1000-feed-fb1b89482663',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfc6ee7-018c-1000-feed-fb1b89482663',
|
||||
identity: 'group 6',
|
||||
configurable: true,
|
||||
userGroups: [],
|
||||
accessPolicies: []
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfc418b-018c-1000-bd42-50d38a5f8ec4',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/users/acfc418b-018c-1000-bd42-50d38a5f8ec4',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfc418b-018c-1000-bd42-50d38a5f8ec4',
|
||||
identity: 'group 4',
|
||||
configurable: true,
|
||||
userGroups: [],
|
||||
accessPolicies: []
|
||||
}
|
||||
}
|
||||
],
|
||||
existingUserGroups: [
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfbfa2c-018c-1000-0311-47b83e34c9c3',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/user-groups/acfbfa2c-018c-1000-0311-47b83e34c9c3',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfbfa2c-018c-1000-0311-47b83e34c9c3',
|
||||
identity: 'group 1',
|
||||
configurable: true,
|
||||
users: [
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'ad0ddd93-018c-1000-4e40-8e3b207abdcd',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'ad0ddd93-018c-1000-4e40-8e3b207abdcd',
|
||||
identity: 'user 1',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'ad0875a3-018c-1000-1877-3f4ebf93f91e',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'ad0875a3-018c-1000-1877-3f4ebf93f91e',
|
||||
identity: 'user 2',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
],
|
||||
accessPolicies: [
|
||||
{
|
||||
revision: {
|
||||
clientId: 'b09bd713-018c-1000-e5b8-14855e466f1b',
|
||||
version: 4
|
||||
},
|
||||
id: 'b0c3148d-018c-1000-2cfe-8fab902c11f7',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'b0c3148d-018c-1000-2cfe-8fab902c11f7',
|
||||
resource: '/system',
|
||||
action: 'read',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'acfcdcf6-018c-1000-604f-84da632fdbd5',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/user-groups/acfcdcf6-018c-1000-604f-84da632fdbd5',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'acfcdcf6-018c-1000-604f-84da632fdbd5',
|
||||
identity: 'group 9',
|
||||
configurable: true,
|
||||
users: [
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'ad0875a3-018c-1000-1877-3f4ebf93f91e',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'ad0875a3-018c-1000-1877-3f4ebf93f91e',
|
||||
identity: 'user 2',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
],
|
||||
accessPolicies: []
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'a69482a2-018c-1000-2c02-fac31f4b102b',
|
||||
uri: 'https://localhost:4200/nifi-api/tenants/user-groups/a69482a2-018c-1000-2c02-fac31f4b102b',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'a69482a2-018c-1000-2c02-fac31f4b102b',
|
||||
identity: 'Group',
|
||||
configurable: true,
|
||||
users: [
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'ad0875a3-018c-1000-1877-3f4ebf93f91e',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'ad0875a3-018c-1000-1877-3f4ebf93f91e',
|
||||
identity: 'user 2',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: '21232f29-7a57-35a7-8389-4a0e4a801fc3',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: '21232f29-7a57-35a7-8389-4a0e4a801fc3',
|
||||
identity: 'admin',
|
||||
configurable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
revision: {
|
||||
version: 0
|
||||
},
|
||||
id: 'abf8bb43-018c-1000-3e0c-a4405f39c934',
|
||||
permissions: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
component: {
|
||||
id: 'abf8bb43-018c-1000-3e0c-a4405f39c934',
|
||||
identity: 'test',
|
||||
configurable: true
|
||||
}
|
||||
}
|
||||
],
|
||||
accessPolicies: []
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [EditTenantDialog, BrowserAnimationsModule],
|
||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
|
||||
});
|
||||
fixture = TestBed.createComponent(EditTenantDialog);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* 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 { EditTenantRequest, EditTenantResponse, Revision, UserEntity, UserGroupEntity } from '../../../state/shared';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
ValidatorFn,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { NifiSpinnerDirective } from '../spinner/nifi-spinner.directive';
|
||||
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { Client } from '../../../service/client.service';
|
||||
import { NiFiCommon } from '../../../service/nifi-common.service';
|
||||
|
||||
@Component({
|
||||
selector: 'edit-tenant-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatDialogModule,
|
||||
MatButtonModule,
|
||||
FormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
MatRadioModule,
|
||||
MatCheckboxModule,
|
||||
NifiSpinnerDirective,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
MatListModule,
|
||||
NgForOf
|
||||
],
|
||||
templateUrl: './edit-tenant-dialog.component.html',
|
||||
styleUrls: ['./edit-tenant-dialog.component.scss']
|
||||
})
|
||||
export class EditTenantDialog {
|
||||
@Input() saving$!: Observable<boolean>;
|
||||
@Output() editTenant: EventEmitter<EditTenantResponse> = new EventEmitter<EditTenantResponse>();
|
||||
@Output() cancel: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
readonly USER: string = 'user';
|
||||
readonly USER_GROUP: string = 'userGroup';
|
||||
isUser: boolean = true;
|
||||
|
||||
identity: FormControl;
|
||||
tenantType: FormControl;
|
||||
editTenantForm: FormGroup;
|
||||
isNew: boolean;
|
||||
|
||||
users: UserEntity[];
|
||||
userGroups: UserGroupEntity[];
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) private request: EditTenantRequest,
|
||||
private formBuilder: FormBuilder,
|
||||
private nifiCommon: NiFiCommon,
|
||||
private client: Client
|
||||
) {
|
||||
const user: UserEntity | undefined = request.user;
|
||||
const userGroup: UserGroupEntity | undefined = request.userGroup;
|
||||
|
||||
if (user || userGroup) {
|
||||
this.isNew = false;
|
||||
|
||||
let identity: string = '';
|
||||
let tenantType: string = this.USER;
|
||||
if (user) {
|
||||
identity = user.component.identity;
|
||||
} else if (userGroup) {
|
||||
identity = userGroup.component.identity;
|
||||
tenantType = this.USER_GROUP;
|
||||
}
|
||||
this.identity = new FormControl(identity, [
|
||||
Validators.required,
|
||||
this.existingTenantValidator(request.existingUsers, request.existingUserGroups, identity)
|
||||
]);
|
||||
this.tenantType = new FormControl({ value: tenantType, disabled: true });
|
||||
} else {
|
||||
this.isNew = true;
|
||||
|
||||
this.identity = new FormControl('', [
|
||||
Validators.required,
|
||||
this.existingTenantValidator(request.existingUsers, request.existingUserGroups)
|
||||
]);
|
||||
this.tenantType = new FormControl(this.USER);
|
||||
}
|
||||
|
||||
this.users = request.existingUsers.slice().sort((a: UserEntity, b: UserEntity) => {
|
||||
return this.nifiCommon.compareString(a.component.identity, b.component.identity);
|
||||
});
|
||||
this.userGroups = request.existingUserGroups.slice().sort((a: UserGroupEntity, b: UserGroupEntity) => {
|
||||
return this.nifiCommon.compareString(a.component.identity, b.component.identity);
|
||||
});
|
||||
|
||||
this.editTenantForm = this.formBuilder.group({
|
||||
identity: this.identity,
|
||||
tenantType: this.tenantType
|
||||
});
|
||||
|
||||
this.tenantTypeChanged();
|
||||
}
|
||||
|
||||
private existingTenantValidator(
|
||||
existingUsers: UserEntity[],
|
||||
existingUserGroups: UserGroupEntity[],
|
||||
currentIdentity?: string
|
||||
): ValidatorFn {
|
||||
const existingUserNames: string[] = existingUsers.map((user) => user.component.identity);
|
||||
const existingUserGroupNames: string[] = existingUserGroups.map((userGroup) => userGroup.component.identity);
|
||||
|
||||
return (control: AbstractControl): ValidationErrors | null => {
|
||||
const value = control.value;
|
||||
if (value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existingTenants: string[] = this.isUser ? existingUserNames : existingUserGroupNames;
|
||||
if (existingTenants.includes(value) && value != currentIdentity) {
|
||||
return {
|
||||
existingTenant: true
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
getIdentityErrorMessage(): string {
|
||||
if (this.identity.hasError('required')) {
|
||||
return 'Identity is required.';
|
||||
}
|
||||
|
||||
const tenantType: string = this.isUser ? 'user' : 'user group';
|
||||
return this.identity.hasError('existingTenant') ? `A ${tenantType} with this identity already exists.` : '';
|
||||
}
|
||||
|
||||
tenantTypeChanged(): void {
|
||||
this.isUser = this.editTenantForm.get('tenantType')?.value == this.USER;
|
||||
if (this.isUser) {
|
||||
this.setupFormWithExistingUserGroups();
|
||||
} else {
|
||||
this.setupFormWithExistingUsers();
|
||||
}
|
||||
}
|
||||
|
||||
setupFormWithExistingUsers(): void {
|
||||
if (this.editTenantForm.contains('userGroups')) {
|
||||
this.editTenantForm.removeControl('userGroups');
|
||||
}
|
||||
|
||||
let users: string[] = [];
|
||||
if (this.request.userGroup) {
|
||||
users.push(...this.request.userGroup.component.users.map((user) => user.id));
|
||||
}
|
||||
|
||||
this.editTenantForm.addControl('users', new FormControl(users));
|
||||
}
|
||||
|
||||
setupFormWithExistingUserGroups(): void {
|
||||
if (this.editTenantForm.contains('users')) {
|
||||
this.editTenantForm.removeControl('users');
|
||||
}
|
||||
|
||||
let userGroups: string[] = [];
|
||||
if (this.request.user) {
|
||||
userGroups.push(...this.request.user.component.userGroups.map((userGroup) => userGroup.id));
|
||||
}
|
||||
|
||||
this.editTenantForm.addControl('userGroups', new FormControl(userGroups));
|
||||
}
|
||||
|
||||
cancelClicked(): void {
|
||||
this.cancel.next();
|
||||
}
|
||||
|
||||
okClicked(): void {
|
||||
if (this.isUser) {
|
||||
const revision: Revision = this.isNew
|
||||
? {
|
||||
clientId: this.client.getClientId(),
|
||||
version: 0
|
||||
}
|
||||
: this.client.getRevision(this.request.user);
|
||||
|
||||
const userGroupsAdded: string[] = [];
|
||||
const userGroupsRemoved: string[] = [];
|
||||
const userGroupsSelected: string[] = this.editTenantForm.get('userGroups')?.value;
|
||||
if (this.request.user) {
|
||||
const userGroups: string[] = this.request.user.component.userGroups.map((userGroup) => userGroup.id);
|
||||
|
||||
userGroupsAdded.push(...userGroupsSelected.filter((x) => !userGroups.includes(x)));
|
||||
userGroupsRemoved.push(...userGroups.filter((x) => !userGroupsSelected.includes(x)));
|
||||
} else {
|
||||
userGroupsAdded.push(...userGroupsSelected);
|
||||
}
|
||||
|
||||
this.editTenant.next({
|
||||
revision,
|
||||
user: {
|
||||
payload: {
|
||||
identity: this.editTenantForm.get('identity')?.value
|
||||
},
|
||||
userGroupsAdded,
|
||||
userGroupsRemoved
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const revision: Revision = this.isNew
|
||||
? {
|
||||
clientId: this.client.getClientId(),
|
||||
version: 0
|
||||
}
|
||||
: this.client.getRevision(this.request.userGroup);
|
||||
|
||||
const users: string[] = this.editTenantForm.get('users')?.value;
|
||||
|
||||
this.editTenant.next({
|
||||
revision,
|
||||
userGroup: {
|
||||
payload: {
|
||||
identity: this.editTenantForm.get('identity')?.value
|
||||
},
|
||||
users
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue