mirror of
https://github.com/apache/nifi.git
synced 2025-02-07 18:48:51 +00:00
parent
7fc27651a4
commit
ecb87149fe
@ -41,6 +41,7 @@ import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { ControllerServiceStateEffects } from './state/contoller-service-state/controller-service-state.effects';
|
||||
import { SystemDiagnosticsEffects } from './state/system-diagnostics/system-diagnostics.effects';
|
||||
import { FlowConfigurationEffects } from './state/flow-configuration/flow-configuration.effects';
|
||||
import { ComponentStateEffects } from './state/component-state/component-state.effects';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
@ -65,7 +66,8 @@ import { FlowConfigurationEffects } from './state/flow-configuration/flow-config
|
||||
FlowConfigurationEffects,
|
||||
StatusHistoryEffects,
|
||||
ControllerServiceStateEffects,
|
||||
SystemDiagnosticsEffects
|
||||
SystemDiagnosticsEffects,
|
||||
ComponentStateEffects
|
||||
),
|
||||
StoreDevtoolsModule.instrument({
|
||||
maxAge: 25,
|
||||
|
@ -55,6 +55,7 @@ import {
|
||||
ContextMenuItemDefinition
|
||||
} from '../../../ui/common/context-menu/context-menu.component';
|
||||
import { promptEmptyQueueRequest, promptEmptyQueuesRequest } from '../state/queue/queue.actions';
|
||||
import { getComponentStateAndOpenDialog } from '../../../state/component-state/component-state.actions';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||
@ -679,13 +680,21 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
|
||||
},
|
||||
{
|
||||
condition: (selection: any) => {
|
||||
// TODO - isStatefulProcessor
|
||||
return false;
|
||||
return this.canvasUtils.isStatefulProcessor(selection);
|
||||
},
|
||||
clazz: 'fa fa-tasks',
|
||||
text: 'View state',
|
||||
action: () => {
|
||||
// TODO - viewState
|
||||
action: (selection: any) => {
|
||||
const selectionData = selection.datum();
|
||||
this.store.dispatch(
|
||||
getComponentStateAndOpenDialog({
|
||||
request: {
|
||||
componentName: selectionData.component.name,
|
||||
componentUri: selectionData.uri,
|
||||
canClear: this.canvasUtils.isConfigurable(selection)
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -464,6 +464,28 @@ export class CanvasUtils {
|
||||
return selection.size() === 1 && selection.classed('funnel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current selection is a stateful processor.
|
||||
*
|
||||
* @param {selection} selection
|
||||
*/
|
||||
public isStatefulProcessor(selection: any): boolean {
|
||||
// ensure the correct number of components are selected
|
||||
if (selection.size() !== 1) {
|
||||
return false;
|
||||
}
|
||||
if (this.canRead(selection) === false || this.canModify(selection) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isProcessor(selection)) {
|
||||
const processorData: any = selection.datum();
|
||||
return processorData.component.persistsState === true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the user can configure or open the policy management page.
|
||||
*/
|
||||
|
@ -48,6 +48,7 @@
|
||||
(configureControllerService)="configureControllerService($event)"
|
||||
(enableControllerService)="enableControllerService($event)"
|
||||
(disableControllerService)="disableControllerService($event)"
|
||||
(viewStateControllerService)="viewStateControllerService($event)"
|
||||
(deleteControllerService)="deleteControllerService($event)"></controller-service-table>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
|
@ -21,15 +21,23 @@ import { ControllerServices } from './controller-services.component';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../state/controller-services/controller-services.reducer';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
describe('ControllerServices', () => {
|
||||
let component: ControllerServices;
|
||||
let fixture: ComponentFixture<ControllerServices>;
|
||||
|
||||
@Component({
|
||||
selector: 'navigation',
|
||||
standalone: true,
|
||||
template: ''
|
||||
})
|
||||
class MockNavigation {}
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ControllerServices],
|
||||
imports: [RouterTestingModule],
|
||||
imports: [RouterTestingModule, MockNavigation],
|
||||
providers: [
|
||||
provideMockStore({
|
||||
initialState
|
||||
|
@ -45,6 +45,7 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
|
||||
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
|
||||
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'controller-services',
|
||||
@ -189,6 +190,18 @@ export class ControllerServices implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
viewStateControllerService(entity: ControllerServiceEntity): void {
|
||||
this.store.dispatch(
|
||||
getComponentStateAndOpenDialog({
|
||||
request: {
|
||||
componentUri: entity.uri,
|
||||
componentName: entity.component.name,
|
||||
canClear: entity.component.state === 'DISABLED'
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteControllerService(entity: ControllerServiceEntity): void {
|
||||
this.store.dispatch(
|
||||
promptControllerServiceDeletion({
|
||||
|
@ -134,7 +134,11 @@
|
||||
*ngIf="canDelete(item)"
|
||||
(click)="deleteClicked(item)"
|
||||
title="Delete"></div>
|
||||
<div class="pointer fa fa-tasks" *ngIf="canViewState(item)" title="View State"></div>
|
||||
<div
|
||||
class="pointer fa fa-tasks"
|
||||
*ngIf="canViewState(item)"
|
||||
(click)="viewStateClicked(item)"
|
||||
title="View State"></div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
@ -60,6 +60,8 @@ export class FlowAnalysisRuleTable {
|
||||
@Output() configureFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> =
|
||||
new EventEmitter<FlowAnalysisRuleEntity>();
|
||||
@Output() enableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>();
|
||||
@Output() viewStateFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> =
|
||||
new EventEmitter<FlowAnalysisRuleEntity>();
|
||||
@Output() disableFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> =
|
||||
new EventEmitter<FlowAnalysisRuleEntity>();
|
||||
|
||||
@ -255,6 +257,10 @@ export class FlowAnalysisRuleTable {
|
||||
return this.canRead(entity) && this.canWrite(entity) && entity.component.persistsState === true;
|
||||
}
|
||||
|
||||
viewStateClicked(entity: FlowAnalysisRuleEntity): void {
|
||||
this.viewStateFlowAnalysisRule.next(entity);
|
||||
}
|
||||
|
||||
select(entity: FlowAnalysisRuleEntity): void {
|
||||
this.selectFlowAnalysisRule.next(entity);
|
||||
}
|
||||
|
@ -35,6 +35,7 @@
|
||||
(selectFlowAnalysisRule)="selectFlowAnalysisRule($event)"
|
||||
(enableFlowAnalysisRule)="enableFlowAnalysisRule($event)"
|
||||
(disableFlowAnalysisRule)="disableFlowAnalysisRule($event)"
|
||||
(viewStateFlowAnalysisRule)="viewStateFlowAnalysisRule($event)"
|
||||
(deleteFlowAnalysisRule)="deleteFlowAnalysisRule($event)"></flow-analysis-rule-table>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
|
@ -41,6 +41,7 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { FlowAnalysisRuleEntity, FlowAnalysisRulesState } from '../../state/flow-analysis-rules';
|
||||
import { CurrentUser } from '../../../../state/current-user';
|
||||
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'flow-analysis-rules',
|
||||
@ -128,6 +129,19 @@ export class FlowAnalysisRules implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
viewStateFlowAnalysisRule(entity: FlowAnalysisRuleEntity): void {
|
||||
const canClear: boolean = entity.status.runStatus === 'DISABLED';
|
||||
this.store.dispatch(
|
||||
getComponentStateAndOpenDialog({
|
||||
request: {
|
||||
componentUri: entity.uri,
|
||||
componentName: entity.component.name,
|
||||
canClear
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteFlowAnalysisRule(entity: FlowAnalysisRuleEntity): void {
|
||||
this.store.dispatch(
|
||||
promptFlowAnalysisRuleDeletion({
|
||||
|
@ -39,6 +39,7 @@
|
||||
(configureControllerService)="configureControllerService($event)"
|
||||
(enableControllerService)="enableControllerService($event)"
|
||||
(disableControllerService)="disableControllerService($event)"
|
||||
(viewStateControllerService)="viewStateControllerService($event)"
|
||||
(deleteControllerService)="deleteControllerService($event)"></controller-service-table>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
|
@ -44,6 +44,7 @@ import { NiFiState } from '../../../../state';
|
||||
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
|
||||
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
|
||||
import { CurrentUser } from '../../../../state/current-user';
|
||||
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'management-controller-services',
|
||||
@ -139,6 +140,18 @@ export class ManagementControllerServices implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
viewStateControllerService(entity: ControllerServiceEntity): void {
|
||||
this.store.dispatch(
|
||||
getComponentStateAndOpenDialog({
|
||||
request: {
|
||||
componentUri: entity.uri,
|
||||
componentName: entity.component.name,
|
||||
canClear: entity.component.state === 'DISABLED'
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
deleteControllerService(entity: ControllerServiceEntity): void {
|
||||
this.store.dispatch(
|
||||
promptControllerServiceDeletion({
|
||||
|
@ -124,7 +124,11 @@
|
||||
*ngIf="canDelete(item)"
|
||||
(click)="deleteClicked(item)"
|
||||
title="Delete"></div>
|
||||
<div class="pointer fa fa-tasks" *ngIf="canViewState(item)" title="View State"></div>
|
||||
<div
|
||||
class="pointer fa fa-tasks"
|
||||
*ngIf="canViewState(item)"
|
||||
(click)="viewStateClicked(item)"
|
||||
title="View State"></div>
|
||||
<div
|
||||
class="pointer fa fa-key"
|
||||
*ngIf="canManageAccessPolicies()"
|
||||
|
@ -51,6 +51,7 @@ export class ReportingTaskTable {
|
||||
@Output() deleteReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
|
||||
@Output() startReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
|
||||
@Output() configureReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
|
||||
@Output() viewStateReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
|
||||
@Output() stopReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
|
||||
|
||||
protected readonly TextTip = TextTip;
|
||||
@ -233,6 +234,10 @@ export class ReportingTaskTable {
|
||||
return this.canRead(entity) && this.canWrite(entity) && entity.component.persistsState === true;
|
||||
}
|
||||
|
||||
viewStateClicked(entity: ReportingTaskEntity): void {
|
||||
this.viewStateReportingTask.next(entity);
|
||||
}
|
||||
|
||||
canManageAccessPolicies(): boolean {
|
||||
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@
|
||||
[currentUser]="currentUser"
|
||||
[flowConfiguration]="(flowConfiguration$ | async)!"
|
||||
(configureReportingTask)="configureReportingTask($event)"
|
||||
(viewStateReportingTask)="viewStateReportingTask($event)"
|
||||
(selectReportingTask)="selectReportingTask($event)"
|
||||
(deleteReportingTask)="deleteReportingTask($event)"
|
||||
(stopReportingTask)="stopReportingTask($event)"
|
||||
|
@ -42,6 +42,7 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
|
||||
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
|
||||
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'reporting-tasks',
|
||||
@ -127,6 +128,19 @@ export class ReportingTasks implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
viewStateReportingTask(entity: ReportingTaskEntity): void {
|
||||
const canClear: boolean = entity.status.runStatus === 'STOPPED' && entity.status.activeThreadCount === 0;
|
||||
this.store.dispatch(
|
||||
getComponentStateAndOpenDialog({
|
||||
request: {
|
||||
componentUri: entity.uri,
|
||||
componentName: entity.component.name,
|
||||
canClear
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
startReportingTask(entity: ReportingTaskEntity): void {
|
||||
this.store.dispatch(
|
||||
startReportingTask({
|
||||
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { ClearComponentStateRequest, LoadComponentStateRequest } from '../state/component-state';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NiFiCommon } from './nifi-common.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ComponentStateService {
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 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, ':');
|
||||
}
|
||||
|
||||
getComponentState(request: LoadComponentStateRequest): Observable<any> {
|
||||
return this.httpClient.get(`${this.stripProtocol(request.componentUri)}/state`);
|
||||
}
|
||||
|
||||
clearComponentState(request: ClearComponentStateRequest): Observable<any> {
|
||||
return this.httpClient.post(`${this.stripProtocol(request.componentUri)}/state/clear-requests`, {});
|
||||
}
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { ComponentStateRequest, ComponentStateResponse } from './index';
|
||||
|
||||
const COMPONENT_STATE_PREFIX = '[Component State]';
|
||||
|
||||
export const getComponentStateAndOpenDialog = createAction(
|
||||
`${COMPONENT_STATE_PREFIX} Get Component State and Open Dialog`,
|
||||
props<{ request: ComponentStateRequest }>()
|
||||
);
|
||||
|
||||
export const loadComponentStateSuccess = createAction(
|
||||
`${COMPONENT_STATE_PREFIX} Load Component State Success`,
|
||||
props<{ response: ComponentStateResponse }>()
|
||||
);
|
||||
|
||||
export const openComponentStateDialog = createAction(`${COMPONENT_STATE_PREFIX} Open Component State Dialog`);
|
||||
|
||||
export const componentStateApiError = createAction(
|
||||
`${COMPONENT_STATE_PREFIX} Component State API error`,
|
||||
props<{ error: string }>()
|
||||
);
|
||||
|
||||
export const clearComponentState = createAction(`${COMPONENT_STATE_PREFIX} Clear Component State`);
|
||||
|
||||
export const reloadComponentState = createAction(`${COMPONENT_STATE_PREFIX} Reload Component State`);
|
||||
|
||||
export const reloadComponentStateSuccess = createAction(
|
||||
`${COMPONENT_STATE_PREFIX} Reload Component State Success`,
|
||||
props<{ response: ComponentStateResponse }>()
|
||||
);
|
||||
|
||||
export const resetComponentState = createAction(`${COMPONENT_STATE_PREFIX} Reset Component State`);
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../index';
|
||||
import * as ComponentStateActions from './component-state.actions';
|
||||
import { catchError, from, map, of, switchMap, tap } from 'rxjs';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ComponentStateService } from '../../service/component-state.service';
|
||||
import { ComponentStateDialog } from '../../ui/common/component-state/component-state.component';
|
||||
import { resetComponentState } from './component-state.actions';
|
||||
import { selectComponentUri } from './component-state.selectors';
|
||||
import { isDefinedAndNotNull } from '../shared';
|
||||
|
||||
@Injectable()
|
||||
export class ComponentStateEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private store: Store<NiFiState>,
|
||||
private componentStateService: ComponentStateService,
|
||||
private dialog: MatDialog
|
||||
) {}
|
||||
|
||||
getComponentStateAndOpenDialog$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ComponentStateActions.getComponentStateAndOpenDialog),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(
|
||||
this.componentStateService.getComponentState({ componentUri: request.componentUri }).pipe(
|
||||
map((response: any) =>
|
||||
ComponentStateActions.loadComponentStateSuccess({
|
||||
response: {
|
||||
componentState: response.componentState
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
ComponentStateActions.componentStateApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
loadComponentStateSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ComponentStateActions.loadComponentStateSuccess),
|
||||
map((action) => action.response),
|
||||
switchMap((response) => of(ComponentStateActions.openComponentStateDialog()))
|
||||
)
|
||||
);
|
||||
|
||||
openComponentStateDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ComponentStateActions.openComponentStateDialog),
|
||||
tap(() => {
|
||||
const dialogReference = this.dialog.open(ComponentStateDialog, {
|
||||
panelClass: 'large-dialog'
|
||||
});
|
||||
|
||||
dialogReference.afterClosed().subscribe((response) => {
|
||||
this.store.dispatch(resetComponentState());
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
clearComponentState$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ComponentStateActions.clearComponentState),
|
||||
concatLatestFrom(() => this.store.select(selectComponentUri).pipe(isDefinedAndNotNull())),
|
||||
switchMap(([action, componentUri]) =>
|
||||
from(
|
||||
this.componentStateService.clearComponentState({ componentUri }).pipe(
|
||||
map((response: any) => ComponentStateActions.reloadComponentState()),
|
||||
catchError((error) =>
|
||||
of(
|
||||
ComponentStateActions.componentStateApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
reloadComponentState$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ComponentStateActions.reloadComponentState),
|
||||
concatLatestFrom(() => this.store.select(selectComponentUri).pipe(isDefinedAndNotNull())),
|
||||
switchMap(([action, componentUri]) =>
|
||||
from(
|
||||
this.componentStateService.getComponentState({ componentUri }).pipe(
|
||||
map((response: any) =>
|
||||
ComponentStateActions.reloadComponentStateSuccess({
|
||||
response: {
|
||||
componentState: response.componentState
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
ComponentStateActions.componentStateApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 { ComponentStateState } from './index';
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import {
|
||||
resetComponentState,
|
||||
loadComponentStateSuccess,
|
||||
componentStateApiError,
|
||||
getComponentStateAndOpenDialog,
|
||||
reloadComponentStateSuccess
|
||||
} from './component-state.actions';
|
||||
|
||||
export const initialState: ComponentStateState = {
|
||||
componentName: null,
|
||||
componentUri: null,
|
||||
componentState: null,
|
||||
canClear: null,
|
||||
status: 'pending',
|
||||
error: null
|
||||
};
|
||||
|
||||
export const componentStateReducer = createReducer(
|
||||
initialState,
|
||||
on(getComponentStateAndOpenDialog, (state, { request }) => ({
|
||||
...state,
|
||||
componentName: request.componentName,
|
||||
componentUri: request.componentUri,
|
||||
canClear: request.canClear,
|
||||
status: 'loading' as const
|
||||
})),
|
||||
on(loadComponentStateSuccess, reloadComponentStateSuccess, (state, { response }) => ({
|
||||
...state,
|
||||
error: null,
|
||||
status: 'success' as const,
|
||||
componentState: response.componentState
|
||||
})),
|
||||
on(componentStateApiError, (state, { error }) => ({
|
||||
...state,
|
||||
error,
|
||||
status: 'error' as const
|
||||
})),
|
||||
on(resetComponentState, (state) => ({
|
||||
...initialState
|
||||
}))
|
||||
);
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { componentStateFeatureKey, ComponentStateState } from './index';
|
||||
|
||||
export const selectComponentStateState = createFeatureSelector<ComponentStateState>(componentStateFeatureKey);
|
||||
|
||||
export const selectComponentState = createSelector(
|
||||
selectComponentStateState,
|
||||
(state: ComponentStateState) => state.componentState
|
||||
);
|
||||
|
||||
export const selectComponentName = createSelector(
|
||||
selectComponentStateState,
|
||||
(state: ComponentStateState) => state.componentName
|
||||
);
|
||||
|
||||
export const selectComponentUri = createSelector(
|
||||
selectComponentStateState,
|
||||
(state: ComponentStateState) => state.componentUri
|
||||
);
|
||||
|
||||
export const selectCanClear = createSelector(selectComponentStateState, (state: ComponentStateState) => state.canClear);
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const componentStateFeatureKey = 'componentState';
|
||||
|
||||
export interface ComponentStateRequest {
|
||||
componentName: string;
|
||||
componentUri: string;
|
||||
canClear: boolean;
|
||||
}
|
||||
|
||||
export interface LoadComponentStateRequest {
|
||||
componentUri: string;
|
||||
}
|
||||
|
||||
export interface ClearComponentStateRequest {
|
||||
componentUri: string;
|
||||
}
|
||||
|
||||
export interface ComponentStateResponse {
|
||||
componentState: ComponentState;
|
||||
}
|
||||
|
||||
export interface StateEntry {
|
||||
key: string;
|
||||
value: string;
|
||||
clusterNodeId?: string;
|
||||
clusterNodeAddress?: string;
|
||||
}
|
||||
|
||||
export interface StateMap {
|
||||
scope: string;
|
||||
state: StateEntry[];
|
||||
totalEntryCount: number;
|
||||
}
|
||||
|
||||
export interface ComponentState {
|
||||
componentId: string;
|
||||
localState?: StateMap;
|
||||
clusterState?: StateMap;
|
||||
stateDescription: string;
|
||||
}
|
||||
|
||||
export interface ComponentStateState {
|
||||
componentName: string | null;
|
||||
componentUri: string | null;
|
||||
componentState: ComponentState | null;
|
||||
canClear: boolean | null;
|
||||
error: string | null;
|
||||
status: 'pending' | 'loading' | 'error' | 'success';
|
||||
}
|
@ -31,6 +31,8 @@ import { systemDiagnosticsFeatureKey, SystemDiagnosticsState } from './system-di
|
||||
import { systemDiagnosticsReducer } from './system-diagnostics/system-diagnostics.reducer';
|
||||
import { flowConfigurationFeatureKey, FlowConfigurationState } from './flow-configuration';
|
||||
import { flowConfigurationReducer } from './flow-configuration/flow-configuration.reducer';
|
||||
import { componentStateFeatureKey, ComponentStateState } from './component-state';
|
||||
import { componentStateReducer } from './component-state/component-state.reducer';
|
||||
|
||||
export interface NiFiState {
|
||||
router: RouterReducerState;
|
||||
@ -41,6 +43,7 @@ export interface NiFiState {
|
||||
[statusHistoryFeatureKey]: StatusHistoryState;
|
||||
[controllerServiceStateFeatureKey]: ControllerServiceState;
|
||||
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
|
||||
[componentStateFeatureKey]: ComponentStateState;
|
||||
}
|
||||
|
||||
export const rootReducers: ActionReducerMap<NiFiState> = {
|
||||
@ -51,5 +54,6 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
|
||||
[flowConfigurationFeatureKey]: flowConfigurationReducer,
|
||||
[statusHistoryFeatureKey]: statusHistoryReducer,
|
||||
[controllerServiceStateFeatureKey]: controllerServiceStateReducer,
|
||||
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer
|
||||
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
|
||||
[componentStateFeatureKey]: componentStateReducer
|
||||
};
|
||||
|
@ -26,7 +26,6 @@ import {
|
||||
reloadStatusHistorySuccess,
|
||||
getStatusHistoryAndOpenDialog
|
||||
} from './status-history.actions';
|
||||
import { produce } from 'immer';
|
||||
|
||||
export const initialState: StatusHistoryState = {
|
||||
statusHistory: {} as StatusHistoryEntity,
|
||||
|
@ -0,0 +1,86 @@
|
||||
<!--
|
||||
~ 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="component-state-dialog" tabindex="0">
|
||||
<h2 mat-dialog-title>Component State</h2>
|
||||
<mat-dialog-content>
|
||||
<div class="flex flex-col justify-between gap-y-5">
|
||||
<div class="flex flex-col" *ngIf="componentName$ | async; let componentName">
|
||||
<div>Name</div>
|
||||
<div class="value">{{ componentName }}</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>Description</div>
|
||||
<div class="value">
|
||||
{{ stateDescription }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="listing-table">
|
||||
<form [formGroup]="filterForm" class="flex flex-col gap-y-2">
|
||||
<div class="value">Displaying {{ filteredEntries }} of {{ totalEntries }}</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<input matInput type="text" class="small" formControlName="filterTerm" />
|
||||
</mat-form-field>
|
||||
<ng-container *ngIf="{ value: (canClear$ | async)! } as canClear">
|
||||
<div *ngIf="canClear.value && totalEntries > 0">
|
||||
<a (click)="clearState()">Clear state</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</form>
|
||||
<div class="h-72 overflow-y-auto overflow-x-hidden border">
|
||||
<table
|
||||
mat-table
|
||||
[dataSource]="dataSource"
|
||||
matSort
|
||||
matSortDisableClear
|
||||
(matSortChange)="sortData($event)"
|
||||
[matSortActive]="initialSortColumn"
|
||||
[matSortDirection]="initialSortDirection">
|
||||
<!-- Key Column -->
|
||||
<ng-container matColumnDef="key">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Key</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.key }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Value Column -->
|
||||
<ng-container matColumnDef="value">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value</th>
|
||||
<td mat-cell *matCellDef="let item" [title]="item.value">
|
||||
{{ item.value }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; let even = even; columns: displayedColumns"
|
||||
[class.even]="even"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="partialResults" class="-mt-3">Showing partial results</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,32 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
.component-state-dialog {
|
||||
@include mat.button-density(-1);
|
||||
|
||||
width: 760px;
|
||||
|
||||
.listing-table {
|
||||
table {
|
||||
.mat-column-key {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ComponentStateDialog } from './component-state.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../../state/component-state/component-state.reducer';
|
||||
|
||||
describe('ComponentStateDialog', () => {
|
||||
let component: ComponentStateDialog;
|
||||
let fixture: ComponentFixture<ComponentStateDialog>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ComponentStateDialog, BrowserAnimationsModule],
|
||||
providers: [provideMockStore({ initialState })]
|
||||
});
|
||||
fixture = TestBed.createComponent(ComponentStateDialog);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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, Input } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { NiFiCommon } from '../../../service/nifi-common.service';
|
||||
import { MatSortModule, Sort } from '@angular/material/sort';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { NifiTooltipDirective } from '../tooltips/nifi-tooltip.directive';
|
||||
import { NifiSpinnerDirective } from '../spinner/nifi-spinner.directive';
|
||||
import { ComponentStateState, StateEntry, StateMap } from '../../../state/component-state';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { clearComponentState } from '../../../state/component-state/component-state.actions';
|
||||
import {
|
||||
selectCanClear,
|
||||
selectComponentName,
|
||||
selectComponentState
|
||||
} from '../../../state/component-state/component-state.selectors';
|
||||
import { isDefinedAndNotNull } from '../../../state/shared';
|
||||
import { debounceTime, Observable } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
@Component({
|
||||
selector: 'component-state',
|
||||
standalone: true,
|
||||
templateUrl: './component-state.component.html',
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
NgIf,
|
||||
NifiTooltipDirective,
|
||||
NifiSpinnerDirective,
|
||||
AsyncPipe,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule
|
||||
],
|
||||
styleUrls: ['./component-state.component.scss', '../../../../assets/styles/listing-table.scss']
|
||||
})
|
||||
export class ComponentStateDialog implements AfterViewInit {
|
||||
@Input() initialSortColumn: 'key' | 'value' = 'key';
|
||||
@Input() initialSortDirection: 'asc' | 'desc' = 'asc';
|
||||
|
||||
componentName$: Observable<string> = this.store.select(selectComponentName).pipe(isDefinedAndNotNull());
|
||||
canClear$: Observable<boolean> = this.store.select(selectCanClear).pipe(isDefinedAndNotNull());
|
||||
|
||||
// TODO - need to include scope column when clustered
|
||||
displayedColumns: string[] = ['key', 'value'];
|
||||
dataSource: MatTableDataSource<StateEntry> = new MatTableDataSource<StateEntry>();
|
||||
|
||||
filterForm: FormGroup;
|
||||
|
||||
stateDescription: string = '';
|
||||
totalEntries: number = 0;
|
||||
filteredEntries: number = 0;
|
||||
partialResults: boolean = false;
|
||||
|
||||
constructor(
|
||||
private store: Store<ComponentStateState>,
|
||||
private formBuilder: FormBuilder,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {
|
||||
this.filterForm = this.formBuilder.group({ filterTerm: '' });
|
||||
|
||||
this.store
|
||||
.select(selectComponentState)
|
||||
.pipe(isDefinedAndNotNull(), takeUntilDestroyed())
|
||||
.subscribe((componentState) => {
|
||||
this.stateDescription = componentState.stateDescription;
|
||||
|
||||
const stateItems: StateEntry[] = [];
|
||||
if (componentState.localState) {
|
||||
const localStateItems: StateEntry[] = this.processStateMap(componentState.localState);
|
||||
stateItems.push(...localStateItems);
|
||||
}
|
||||
if (componentState.clusterState) {
|
||||
const clusterStateItems: StateEntry[] = this.processStateMap(componentState.clusterState);
|
||||
stateItems.push(...clusterStateItems);
|
||||
}
|
||||
|
||||
this.dataSource.data = this.sortStateEntries(stateItems, {
|
||||
active: this.initialSortColumn,
|
||||
direction: this.initialSortDirection
|
||||
});
|
||||
this.filteredEntries = stateItems.length;
|
||||
|
||||
// apply any filtering to the new data
|
||||
const filterTerm = this.filterForm.get('filterTerm')?.value;
|
||||
if (filterTerm?.length > 0) {
|
||||
this.applyFilter(filterTerm);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.filterForm
|
||||
.get('filterTerm')
|
||||
?.valueChanges.pipe(debounceTime(500))
|
||||
.subscribe((filterTerm: string) => {
|
||||
this.applyFilter(filterTerm);
|
||||
});
|
||||
}
|
||||
|
||||
processStateMap(stateMap: StateMap): StateEntry[] {
|
||||
const stateItems: StateEntry[] = stateMap.state ? stateMap.state : [];
|
||||
|
||||
if (stateItems.length !== stateMap.totalEntryCount) {
|
||||
this.partialResults = true;
|
||||
}
|
||||
|
||||
this.totalEntries += stateMap.totalEntryCount;
|
||||
|
||||
return stateItems;
|
||||
}
|
||||
|
||||
applyFilter(filterTerm: string) {
|
||||
this.dataSource.filter = filterTerm.trim().toLowerCase();
|
||||
this.filteredEntries = this.dataSource.filteredData.length;
|
||||
}
|
||||
|
||||
sortData(sort: Sort) {
|
||||
this.dataSource.data = this.sortStateEntries(this.dataSource.data, sort);
|
||||
}
|
||||
|
||||
private sortStateEntries(data: StateEntry[], sort: Sort): StateEntry[] {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.slice().sort((a, b) => {
|
||||
const isAsc = sort.direction === 'asc';
|
||||
let retVal = 0;
|
||||
switch (sort.active) {
|
||||
case 'key':
|
||||
retVal = this.nifiCommon.compareString(a.key, b.key);
|
||||
break;
|
||||
case 'value':
|
||||
retVal = this.nifiCommon.compareString(a.value, b.value);
|
||||
break;
|
||||
}
|
||||
return retVal * (isAsc ? 1 : -1);
|
||||
});
|
||||
}
|
||||
|
||||
clearState(): void {
|
||||
this.store.dispatch(clearComponentState());
|
||||
}
|
||||
}
|
@ -141,7 +141,11 @@
|
||||
*ngIf="canDelete(item)"
|
||||
(click)="deleteClicked(item, $event)"
|
||||
title="Delete"></div>
|
||||
<div class="pointer fa fa-tasks" *ngIf="canViewState(item)" title="View State"></div>
|
||||
<div
|
||||
class="pointer fa fa-tasks"
|
||||
*ngIf="canViewState(item)"
|
||||
(click)="viewStateClicked(item)"
|
||||
title="View State"></div>
|
||||
<div
|
||||
class="pointer fa fa-key"
|
||||
*ngIf="canManageAccessPolicies()"
|
||||
|
@ -80,6 +80,8 @@ export class ControllerServiceTable {
|
||||
new EventEmitter<ControllerServiceEntity>();
|
||||
@Output() disableControllerService: EventEmitter<ControllerServiceEntity> =
|
||||
new EventEmitter<ControllerServiceEntity>();
|
||||
@Output() viewStateControllerService: EventEmitter<ControllerServiceEntity> =
|
||||
new EventEmitter<ControllerServiceEntity>();
|
||||
|
||||
protected readonly TextTip = TextTip;
|
||||
protected readonly BulletinsTip = BulletinsTip;
|
||||
@ -251,6 +253,10 @@ export class ControllerServiceTable {
|
||||
return this.canRead(entity) && this.canWrite(entity) && entity.component.persistsState === true;
|
||||
}
|
||||
|
||||
viewStateClicked(entity: ControllerServiceEntity): void {
|
||||
this.viewStateControllerService.next(entity);
|
||||
}
|
||||
|
||||
canManageAccessPolicies(): boolean {
|
||||
return this.flowConfiguration.supportsManagedAuthorizer && this.currentUser.tenantsPermissions.canRead;
|
||||
}
|
||||
|
@ -100,7 +100,6 @@
|
||||
<button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
|
||||
<button
|
||||
[disabled]="selectedType == null || saving"
|
||||
type="submit"
|
||||
color="primary"
|
||||
(click)="createExtension(selectedType)"
|
||||
mat-raised-button>
|
||||
|
Loading…
x
Reference in New Issue
Block a user