mirror of https://github.com/apache/nifi.git
NIFI-13248: Add Flow Analysis report menu to new ui (#8974)
* NIFI-13248: Add Flow Analysis report menu to new ui * NIFI-13248: use ngrx to manage state for flow analysis component * NIFI-13248: update types used in flow analysis drawer * NIFI-13248: add violation details dialog * NIFI-13248: add go to component functionality * NIFI-13248: use Tailwind classes * NIFI-13248: update analysis status icon based on response * NIFI-13248: fix broken unit tests * NIFI-13248: add missing license headers * NIFI-13248: refactor styling patch provided by @scottyaslan * NIFI-13248: fix broken styling * NIFI-13248: further style refactoring patch provided by @scottyaslan * NIFI-13248: binding and spacing fixes * NIFI-13248: wire up pg name and id * NIFI-13248: use breadcrumb selector to obtain process group name * NIFI-13248: remove border color classes * NIFI-13248: update drawer button icon
This commit is contained in:
parent
9fbe6aab74
commit
8e0c4aeb33
|
@ -30,6 +30,7 @@ import { ControllerServicesEffects } from '../state/controller-services/controll
|
|||
import { ParameterEffects } from '../state/parameter/parameter.effects';
|
||||
import { QueueEffects } from '../state/queue/queue.effects';
|
||||
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
|
||||
import { FlowAnalysisEffects } from '../state/flow-analysis/flow-analysis.effects';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FlowDesigner, VersionControlTip],
|
||||
|
@ -43,7 +44,8 @@ import { BannerText } from '../../../ui/common/banner-text/banner-text.component
|
|||
TransformEffects,
|
||||
ControllerServicesEffects,
|
||||
ParameterEffects,
|
||||
QueueEffects
|
||||
QueueEffects,
|
||||
FlowAnalysisEffects
|
||||
),
|
||||
NgOptimizedImage,
|
||||
MatDialogModule,
|
||||
|
|
|
@ -35,6 +35,8 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
|
|||
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('ConnectableBehavior', () => {
|
||||
let service: ConnectableBehavior;
|
||||
|
@ -45,7 +47,8 @@ describe('ConnectableBehavior', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -36,6 +36,8 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
|
|||
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('DraggableBehavior', () => {
|
||||
let service: DraggableBehavior;
|
||||
|
@ -46,7 +48,8 @@ describe('DraggableBehavior', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -36,6 +36,8 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
|
|||
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('EditableBehaviorService', () => {
|
||||
let service: EditableBehavior;
|
||||
|
@ -45,7 +47,8 @@ describe('EditableBehaviorService', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -35,6 +35,8 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
|
|||
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('QuickSelectBehavior', () => {
|
||||
let service: QuickSelectBehavior;
|
||||
|
@ -45,7 +47,8 @@ describe('QuickSelectBehavior', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -34,6 +34,8 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
|
|||
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('SelectableBehavior', () => {
|
||||
let service: SelectableBehavior;
|
||||
|
@ -44,7 +46,8 @@ describe('SelectableBehavior', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -36,6 +36,8 @@ import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-
|
|||
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../queue/state';
|
||||
import * as fromQueue from '../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('BirdseyeView', () => {
|
||||
let service: BirdseyeView;
|
||||
|
@ -46,7 +48,8 @@ describe('BirdseyeView', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -35,6 +35,8 @@ import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-
|
|||
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../queue/state';
|
||||
import * as fromQueue from '../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('CanvasUtils', () => {
|
||||
let service: CanvasUtils;
|
||||
|
@ -45,7 +47,8 @@ describe('CanvasUtils', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -36,6 +36,8 @@ import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-
|
|||
import * as fromFlowConfiguration from '../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../queue/state';
|
||||
import * as fromQueue from '../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('CanvasView', () => {
|
||||
let service: CanvasView;
|
||||
|
@ -46,7 +48,8 @@ describe('CanvasView', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FlowAnalysisService } from './flow-analysis.service';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
describe('FlowAnalysisService', () => {
|
||||
let service: FlowAnalysisService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideMockStore({}),
|
||||
{
|
||||
provide: HttpClient,
|
||||
useValue: {}
|
||||
}
|
||||
]
|
||||
});
|
||||
service = TestBed.inject(FlowAnalysisService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FlowAnalysisService {
|
||||
private static readonly API: string = '../nifi-api';
|
||||
|
||||
constructor(private httpClient: HttpClient) {}
|
||||
|
||||
getResults(processGroupId: string): Observable<any> {
|
||||
return this.httpClient.get(`${FlowAnalysisService.API}/flow/flow-analysis/results/${processGroupId}`);
|
||||
}
|
||||
}
|
|
@ -37,6 +37,8 @@ import * as fromFlowConfiguration from '../../../../state/flow-configuration/flo
|
|||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('ConnectionManager', () => {
|
||||
let service: ConnectionManager;
|
||||
|
@ -47,7 +49,8 @@ describe('ConnectionManager', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -36,6 +36,8 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
|
|||
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('FunnelManager', () => {
|
||||
let service: FunnelManager;
|
||||
|
@ -46,7 +48,8 @@ describe('FunnelManager', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -37,6 +37,8 @@ import * as fromFlowConfiguration from '../../../../state/flow-configuration/flo
|
|||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { ClusterConnectionService } from '../../../../service/cluster-connection.service';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('LabelManager', () => {
|
||||
let service: LabelManager;
|
||||
|
@ -47,7 +49,8 @@ describe('LabelManager', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -36,6 +36,8 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
|
|||
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('PortManager', () => {
|
||||
let service: PortManager;
|
||||
|
@ -46,7 +48,8 @@ describe('PortManager', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -36,6 +36,8 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
|
|||
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('ProcessGroupManager', () => {
|
||||
let service: ProcessGroupManager;
|
||||
|
@ -46,7 +48,8 @@ describe('ProcessGroupManager', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -36,6 +36,8 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
|
|||
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('ProcessorManager', () => {
|
||||
let service: ProcessorManager;
|
||||
|
@ -46,7 +48,8 @@ describe('ProcessorManager', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -36,6 +36,8 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
|
|||
import * as fromFlowConfiguration from '../../../../state/flow-configuration/flow-configuration.reducer';
|
||||
import { queueFeatureKey } from '../../../queue/state';
|
||||
import * as fromQueue from '../../state/queue/queue.reducer';
|
||||
import { flowAnalysisFeatureKey } from '../../state/flow-analysis';
|
||||
import * as fromFlowAnalysis from '../../state/flow-analysis/flow-analysis.reducer';
|
||||
|
||||
describe('RemoteProcessGroupManager', () => {
|
||||
let service: RemoteProcessGroupManager;
|
||||
|
@ -46,7 +48,8 @@ describe('RemoteProcessGroupManager', () => {
|
|||
[transformFeatureKey]: fromTransform.initialState,
|
||||
[controllerServicesFeatureKey]: fromControllerServices.initialState,
|
||||
[parameterFeatureKey]: fromParameter.initialState,
|
||||
[queueFeatureKey]: fromQueue.initialState
|
||||
[queueFeatureKey]: fromQueue.initialState,
|
||||
[flowAnalysisFeatureKey]: fromFlowAnalysis.initialState
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
|
|
@ -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 { createAction, props } from '@ngrx/store';
|
||||
import { FlowAnalysisRequestResponse, FlowAnalysisRule, FlowAnalysisRuleViolation } from '.';
|
||||
|
||||
export const startPollingFlowAnalysis = createAction('[Flow Analysis] Start Polling Flow Analysis');
|
||||
|
||||
export const stopPollingFlowAnalysis = createAction('[Flow Analysis] Stop Polling Flow Analysis');
|
||||
|
||||
export const pollFlowAnalysis = createAction(`[Flow Analysis] Poll Flow Analysis`);
|
||||
|
||||
export const pollFlowAnalysisSuccess = createAction(
|
||||
`[Flow Analysis] Poll Flow Analysis Success`,
|
||||
props<{ response: FlowAnalysisRequestResponse }>()
|
||||
);
|
||||
|
||||
export const flowAnalysisApiError = createAction('[Flow Analysis] API Error', props<{ error: string }>());
|
||||
|
||||
export const resetPollingFlowAnalysis = createAction(`[Flow Analysis] Reset Polling Flow Analysis`);
|
||||
|
||||
export const navigateToEditFlowAnalysisRule = createAction(
|
||||
'[Flow Analysis Rules] Navigate To Edit Flow Analysis Rule',
|
||||
props<{ id: string }>()
|
||||
);
|
||||
|
||||
export const openRuleDetailsDialog = createAction(
|
||||
'[Flow Analysis Rules] Open Flow Analysis Rule Details Dialog',
|
||||
props<{ violation: FlowAnalysisRuleViolation; rule: FlowAnalysisRule }>()
|
||||
);
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 { concatLatestFrom } from '@ngrx/operators';
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { asyncScheduler, catchError, from, interval, map, of, startWith, switchMap, takeUntil, tap } from 'rxjs';
|
||||
import * as FlowAnalysisActions from './flow-analysis.actions';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { FlowAnalysisService } from '../../service/flow-analysis.service';
|
||||
import { ErrorHelper } from 'apps/nifi/src/app/service/error-helper.service';
|
||||
import { selectCurrentProcessGroupId } from '../flow/flow.selectors';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { LARGE_DIALOG } from '@nifi/shared';
|
||||
import { ViolationDetailsDialogComponent } from '../../ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component';
|
||||
|
||||
@Injectable()
|
||||
export class FlowAnalysisEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private store: Store<NiFiState>,
|
||||
private flowAnalysisService: FlowAnalysisService,
|
||||
private errorHelper: ErrorHelper,
|
||||
private router: Router,
|
||||
private dialog: MatDialog
|
||||
) {}
|
||||
|
||||
startPollingFlowAnalysis$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowAnalysisActions.startPollingFlowAnalysis),
|
||||
switchMap(() =>
|
||||
interval(30000, asyncScheduler).pipe(
|
||||
startWith(0),
|
||||
takeUntil(this.actions$.pipe(ofType(FlowAnalysisActions.stopPollingFlowAnalysis)))
|
||||
)
|
||||
),
|
||||
switchMap(() => of(FlowAnalysisActions.pollFlowAnalysis()))
|
||||
)
|
||||
);
|
||||
|
||||
resetPollingFlowAnalysis$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowAnalysisActions.resetPollingFlowAnalysis),
|
||||
switchMap(() => {
|
||||
this.store.dispatch(FlowAnalysisActions.stopPollingFlowAnalysis());
|
||||
return of(FlowAnalysisActions.pollFlowAnalysis());
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
pollFlowAnalysis$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowAnalysisActions.pollFlowAnalysis),
|
||||
concatLatestFrom(() => this.store.select(selectCurrentProcessGroupId)),
|
||||
switchMap(([, pgId]) => {
|
||||
return from(this.flowAnalysisService.getResults(pgId)).pipe(
|
||||
map((response) =>
|
||||
FlowAnalysisActions.pollFlowAnalysisSuccess({
|
||||
response: response
|
||||
})
|
||||
),
|
||||
catchError((errorResponse: HttpErrorResponse) => {
|
||||
this.store.dispatch(FlowAnalysisActions.stopPollingFlowAnalysis());
|
||||
return of(
|
||||
FlowAnalysisActions.flowAnalysisApiError({
|
||||
error: this.errorHelper.getErrorString(errorResponse)
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
navigateToEditFlowAnalysisRule$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowAnalysisActions.navigateToEditFlowAnalysisRule),
|
||||
map((action) => action.id),
|
||||
tap((id) => {
|
||||
this.router.navigate(['/settings', 'flow-analysis-rules', id, 'edit']);
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
openRuleDetailsDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowAnalysisActions.openRuleDetailsDialog),
|
||||
tap(({ violation, rule }) => {
|
||||
this.dialog.open(ViolationDetailsDialogComponent, {
|
||||
...LARGE_DIALOG,
|
||||
data: {
|
||||
violation,
|
||||
rule
|
||||
}
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
}
|
|
@ -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 { createReducer, on } from '@ngrx/store';
|
||||
import { FlowAnalysisState } from './index';
|
||||
import { pollFlowAnalysis, pollFlowAnalysisSuccess } from './flow-analysis.actions';
|
||||
|
||||
export const initialState: FlowAnalysisState = {
|
||||
rules: [],
|
||||
ruleViolations: [],
|
||||
flowAnalysisPending: false,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
export const flowAnalysisReducer = createReducer(
|
||||
initialState,
|
||||
on(pollFlowAnalysis, (state) => ({
|
||||
...state,
|
||||
status: 'loading' as const
|
||||
})),
|
||||
on(pollFlowAnalysisSuccess, (state, { response }) => ({
|
||||
...state,
|
||||
rules: response.rules,
|
||||
ruleViolations: response.ruleViolations,
|
||||
flowAnalysisPending: response.flowAnalysisPending,
|
||||
status: 'success' as const
|
||||
}))
|
||||
);
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createSelector } from '@ngrx/store';
|
||||
import { flowAnalysisFeatureKey } from './index';
|
||||
import { CanvasState, selectCanvasState } from '../index';
|
||||
|
||||
export const selectFlowAnalysisState = createSelector(
|
||||
selectCanvasState,
|
||||
(state: CanvasState) => state[flowAnalysisFeatureKey]
|
||||
);
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { PropertyDescriptor, Bundle, Permissions } from '../../../../state/shared';
|
||||
|
||||
export const flowAnalysisFeatureKey = 'flowAnalysis';
|
||||
|
||||
export interface FlowAnalysisRule {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
bundle: Bundle;
|
||||
state: string;
|
||||
comments?: string;
|
||||
persistsState: boolean;
|
||||
restricted: boolean;
|
||||
deprecated: boolean;
|
||||
extensionMissing: boolean;
|
||||
multipleVersionsAvailable: boolean;
|
||||
supportsSensitiveDynamicProperties: boolean;
|
||||
enforcementPolicy: string;
|
||||
properties: { [key: string]: string };
|
||||
descriptors: { [key: string]: PropertyDescriptor };
|
||||
sensitiveDynamicPropertyNames: string[];
|
||||
validationErrors: string[];
|
||||
validationStatus: 'VALID' | 'INVALID' | 'VALIDATING';
|
||||
}
|
||||
|
||||
export interface FlowAnalysisRuleViolation {
|
||||
enforcementPolicy: string;
|
||||
scope: string;
|
||||
subjectId: string;
|
||||
subjectDisplayName: string;
|
||||
groupId: string;
|
||||
ruleId: string;
|
||||
issueId: string;
|
||||
violationMessage: string;
|
||||
subjectComponentType: string;
|
||||
subjectPermissionDto: Permissions;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface FlowAnalysisRequestResponse {
|
||||
rules: FlowAnalysisRule[];
|
||||
ruleViolations: FlowAnalysisRuleViolation[];
|
||||
flowAnalysisPending: boolean;
|
||||
}
|
||||
|
||||
export interface FlowAnalysisState {
|
||||
rules: FlowAnalysisRule[];
|
||||
ruleViolations: FlowAnalysisRuleViolation[];
|
||||
flowAnalysisPending: boolean;
|
||||
status: 'pending' | 'loading' | 'success';
|
||||
}
|
|
@ -573,6 +573,11 @@ export const setOperationCollapsed = createAction(
|
|||
props<{ operationCollapsed: boolean }>()
|
||||
);
|
||||
|
||||
export const setFlowAnalysisOpen = createAction(
|
||||
`${CANVAS_PREFIX} Set Flow Analysis Open`,
|
||||
props<{ flowAnalysisOpen: boolean }>()
|
||||
);
|
||||
|
||||
/*
|
||||
General
|
||||
*/
|
||||
|
|
|
@ -153,6 +153,7 @@ import {
|
|||
import { VerifyPropertiesRequestContext } from '../../../../state/property-verification';
|
||||
import { BackNavigation } from '../../../../state/navigation';
|
||||
import { Storage, NiFiCommon } from '@nifi/shared';
|
||||
import { resetPollingFlowAnalysis } from '../flow-analysis/flow-analysis.actions';
|
||||
|
||||
@Injectable()
|
||||
export class FlowEffects {
|
||||
|
@ -212,6 +213,7 @@ export class FlowEffects {
|
|||
this.flowService.getControllerBulletins()
|
||||
]).pipe(
|
||||
map(([flow, flowStatus, controllerBulletins]) => {
|
||||
this.store.dispatch(resetPollingFlowAnalysis());
|
||||
return FlowActions.loadProcessGroupSuccess({
|
||||
response: {
|
||||
id: request.id,
|
||||
|
|
|
@ -60,6 +60,7 @@ import {
|
|||
saveToFlowRegistrySuccess,
|
||||
setAllowTransition,
|
||||
setDragging,
|
||||
setFlowAnalysisOpen,
|
||||
setNavigationCollapsed,
|
||||
setOperationCollapsed,
|
||||
setSkipTransform,
|
||||
|
@ -164,6 +165,7 @@ export const initialState: FlowState = {
|
|||
allowTransition: false,
|
||||
navigationCollapsed: false,
|
||||
operationCollapsed: false,
|
||||
flowAnalysisOpen: false,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
|
@ -541,6 +543,10 @@ export const flowReducer = createReducer(
|
|||
...state,
|
||||
operationCollapsed
|
||||
})),
|
||||
on(setFlowAnalysisOpen, (state, { flowAnalysisOpen }) => ({
|
||||
...state,
|
||||
flowAnalysisOpen
|
||||
})),
|
||||
on(
|
||||
startComponentSuccess,
|
||||
stopComponentSuccess,
|
||||
|
|
|
@ -257,3 +257,5 @@ export const selectMaxZIndex = (componentType: ComponentType.Connection | Compon
|
|||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const selectFlowAnalysisOpen = createSelector(selectFlowState, (state: FlowState) => state.flowAnalysisOpen);
|
||||
|
|
|
@ -638,6 +638,7 @@ export interface FlowState {
|
|||
saving: boolean;
|
||||
navigationCollapsed: boolean;
|
||||
operationCollapsed: boolean;
|
||||
flowAnalysisOpen: boolean;
|
||||
versionSaving: boolean;
|
||||
changeVersionRequest: FlowUpdateRequestEntity | null;
|
||||
copiedSnippet: CopiedSnippet | null;
|
||||
|
|
|
@ -31,6 +31,8 @@ import { parameterReducer } from './parameter/parameter.reducer';
|
|||
import { queueFeatureKey } from '../../queue/state';
|
||||
import { QueueState } from './queue';
|
||||
import { queueReducer } from './queue/queue.reducer';
|
||||
import { FlowAnalysisState, flowAnalysisFeatureKey } from './flow-analysis';
|
||||
import { flowAnalysisReducer } from './flow-analysis/flow-analysis.reducer';
|
||||
|
||||
export const canvasFeatureKey = 'canvas';
|
||||
|
||||
|
@ -40,6 +42,7 @@ export interface CanvasState {
|
|||
[controllerServicesFeatureKey]: ControllerServicesState;
|
||||
[parameterFeatureKey]: ParameterState;
|
||||
[queueFeatureKey]: QueueState;
|
||||
[flowAnalysisFeatureKey]: FlowAnalysisState;
|
||||
}
|
||||
|
||||
export function reducers(state: CanvasState | undefined, action: Action) {
|
||||
|
@ -48,7 +51,8 @@ export function reducers(state: CanvasState | undefined, action: Action) {
|
|||
[transformFeatureKey]: transformReducer,
|
||||
[controllerServicesFeatureKey]: controllerServicesReducer,
|
||||
[parameterFeatureKey]: parameterReducer,
|
||||
[queueFeatureKey]: queueReducer
|
||||
[queueFeatureKey]: queueReducer,
|
||||
[flowAnalysisFeatureKey]: flowAnalysisReducer
|
||||
})(state, action);
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,14 @@
|
|||
);
|
||||
}
|
||||
|
||||
mat-sidenav {
|
||||
background-color: if(
|
||||
$is-dark,
|
||||
$supplemental-theme-surface-palette-darker,
|
||||
$supplemental-theme-surface-palette-lighter
|
||||
);
|
||||
}
|
||||
|
||||
/* svg styles */
|
||||
|
||||
svg.canvas-svg {
|
||||
|
|
|
@ -18,12 +18,19 @@
|
|||
<div class="flex flex-col h-full">
|
||||
<fd-header></fd-header>
|
||||
<div class="flex-1">
|
||||
<mat-sidenav-container class="h-full">
|
||||
<mat-sidenav mode="side" [opened]="flowAnalysisOpen()" position="end">
|
||||
<flow-analysis-drawer></flow-analysis-drawer>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content>
|
||||
<graph-controls></graph-controls>
|
||||
<div
|
||||
id="canvas-container"
|
||||
class="canvas-background select-none h-full"
|
||||
class="canvas-background select-none h-full w-full"
|
||||
[cdkContextMenuTriggerFor]="contextMenu.menu"></div>
|
||||
<fd-context-menu #contextMenu [menuProvider]="canvasContextMenu" menuId="root"></fd-context-menu>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
</div>
|
||||
<fd-footer></fd-footer>
|
||||
</div>
|
||||
|
|
|
@ -30,6 +30,10 @@ import { HeaderComponent } from './header/header.component';
|
|||
import { FooterComponent } from './footer/footer.component';
|
||||
import { canvasFeatureKey } from '../../state';
|
||||
import { flowFeatureKey } from '../../state/flow';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { FlowAnalysisDrawerComponent } from './header/flow-analysis-drawer/flow-analysis-drawer.component';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { CanvasActionsService } from '../../service/canvas-actions.service';
|
||||
|
||||
describe('Canvas', () => {
|
||||
let component: Canvas;
|
||||
|
@ -54,9 +58,12 @@ describe('Canvas', () => {
|
|||
imports: [
|
||||
CdkContextMenuTrigger,
|
||||
ContextMenu,
|
||||
MatSidenavModule,
|
||||
NoopAnimationsModule,
|
||||
MockComponent(GraphControls),
|
||||
MockComponent(HeaderComponent),
|
||||
MockComponent(FooterComponent)
|
||||
MockComponent(FooterComponent),
|
||||
FlowAnalysisDrawerComponent
|
||||
],
|
||||
providers: [
|
||||
provideMockStore({
|
||||
|
@ -71,7 +78,8 @@ describe('Canvas', () => {
|
|||
value: breadcrumbEntity
|
||||
}
|
||||
]
|
||||
})
|
||||
}),
|
||||
CanvasActionsService
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(Canvas);
|
||||
|
|
|
@ -43,6 +43,7 @@ import {
|
|||
selectConnection,
|
||||
selectCurrentProcessGroupId,
|
||||
selectEditedCurrentProcessGroup,
|
||||
selectFlowAnalysisOpen,
|
||||
selectFunnel,
|
||||
selectInputPort,
|
||||
selectLabel,
|
||||
|
@ -80,6 +81,8 @@ export class Canvas implements OnInit, OnDestroy {
|
|||
private scale: number = INITIAL_SCALE;
|
||||
private canvasClicked = false;
|
||||
|
||||
flowAnalysisOpen = this.store.selectSignal(selectFlowAnalysisOpen);
|
||||
|
||||
constructor(
|
||||
private store: Store<CanvasState>,
|
||||
private canvasView: CanvasView,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { Canvas } from './canvas.component';
|
||||
import { ContextMenu } from '../../../../ui/common/context-menu/context-menu.component';
|
||||
import { CdkContextMenuTrigger, CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
|
||||
|
@ -24,6 +25,7 @@ import { GraphControls } from './graph-controls/graph-controls.component';
|
|||
import { CanvasRoutingModule } from './canvas-routing.module';
|
||||
import { HeaderComponent } from './header/header.component';
|
||||
import { FooterComponent } from './footer/footer.component';
|
||||
import { FlowAnalysisDrawerComponent } from './header/flow-analysis-drawer/flow-analysis-drawer.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [Canvas],
|
||||
|
@ -38,7 +40,9 @@ import { FooterComponent } from './footer/footer.component';
|
|||
GraphControls,
|
||||
ContextMenu,
|
||||
HeaderComponent,
|
||||
FooterComponent
|
||||
FooterComponent,
|
||||
MatSidenavModule,
|
||||
FlowAnalysisDrawerComponent
|
||||
]
|
||||
})
|
||||
export class CanvasModule {}
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
<!--
|
||||
~ 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="flow-analysis-drawer h-full w-96 p-5">
|
||||
<component-context type="ProcessGroup" [name]="processGroupName" [id]="currentProcessGroupId"></component-context>
|
||||
<div class="flex items-center w-full">
|
||||
<ng-container *ngIf="isAnalysisPending">
|
||||
<span *nifiSpinner="isAnalysisPending"></span>
|
||||
<div class="ml-1">Rules analysis pending...</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div>
|
||||
<mat-checkbox class="text-sm" color="primary" [(ngModel)]="showEnforcedViolations">
|
||||
Show enforced violations
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div>
|
||||
<mat-checkbox color="primary" [(ngModel)]="showWarningViolations">
|
||||
Show warning violations
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 mb-2" [hidden]="showEnforcedViolations() || showWarningViolations()">
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<mat-expansion-panel hideToggle>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<div class="flex flex-1 justify-start">
|
||||
<div>Enforced Rules ({{ enforcedRules.length }})</div>
|
||||
</div>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
@if (rules) {
|
||||
@for (rule of enforcedRules; track rule.id) {
|
||||
<div class="mb-2">
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center">{{ rule.name }}</div>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
[matMenuTriggerFor]="menu"
|
||||
class="h-16 w-16 flex items-center justify-center">
|
||||
<i class="fa fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="openDocumentation(rule)">
|
||||
<i class="fa fa-book mr-2 primary-color"></i>
|
||||
View Documentation
|
||||
</button>
|
||||
<button mat-menu-item (click)="openRule(rule)">
|
||||
<i class="fa fa-cog mr-2 primary-color"></i>
|
||||
Edit Rule
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
@if (violationsMap.size > 0 && violationsMap.get(rule.id)) {
|
||||
<div class="warn-color-darker text-sm">
|
||||
<ng-container [ngPlural]="violationsMap.get(rule.id).length">
|
||||
<ng-template ngPluralCase="=1"
|
||||
>{{ violationsMap.get(rule.id).length }} violation</ng-template
|
||||
>
|
||||
<ng-template ngPluralCase="other"
|
||||
>{{ violationsMap.get(rule.id).length }} violations</ng-template
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
@for (violation of violationsMap.get(rule.id); track violation.scope) {
|
||||
<div class="flex align-center justify-between mt-2">
|
||||
<div class="flex flex-col items-start ml-2">
|
||||
<div *ngIf="violation?.subjectPermissionDto?.canRead; else unauthorized">
|
||||
{{ violation.subjectDisplayName }}
|
||||
</div>
|
||||
<span class="text-sm">
|
||||
{{ violation.subjectId }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[ngTemplateOutlet]="violationMenuTemplate"
|
||||
[ngTemplateOutletContext]="{ violation: violation }"></ng-template>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel hideToggle>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
<div class="flex flex-1 justify-start">
|
||||
<div>Warning Rules ({{ warningRules.length }})</div>
|
||||
</div>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
@if (rules) {
|
||||
@for (rule of warningRules; track rule.id) {
|
||||
<div>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center">{{ rule.name }}</div>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
[matMenuTriggerFor]="menu"
|
||||
class="h-16 w-16 flex items-center justify-center">
|
||||
<i class="fa fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu" class="rule-menu w-52 shadow-lg">
|
||||
<button mat-menu-item (click)="openDocumentation(rule)">
|
||||
<i class="fa fa-book mr-2 primary-color"></i>
|
||||
View Documentation
|
||||
</button>
|
||||
<button mat-menu-item (click)="openRule(rule)">
|
||||
<i class="fa fa-cog mr-2 primary-color"></i>
|
||||
Edit Rule
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
@if (violationsMap.size > 0 && violationsMap.get(rule.id)) {
|
||||
<div class="primary-color text-sm">
|
||||
<ng-container [ngPlural]="violationsMap.get(rule.id).length">
|
||||
<ng-template ngPluralCase="=1"
|
||||
>{{ violationsMap.get(rule.id).length }} violation</ng-template
|
||||
>
|
||||
<ng-template ngPluralCase="other"
|
||||
>{{ violationsMap.get(rule.id).length }} violations</ng-template
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ul>
|
||||
@for (violation of violationsMap.get(rule.id); track violation.scope) {
|
||||
<li class="flex align-center justify-between mt-2">
|
||||
<div class="flex flex-col items-start ml-2">
|
||||
<div *ngIf="violation?.subjectPermissionDto?.canRead; else unauthorized">
|
||||
{{ violation.subjectDisplayName }}
|
||||
</div>
|
||||
<span class="text-sm">
|
||||
{{ violation.subjectId }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[ngTemplateOutlet]="violationMenuTemplate"
|
||||
[ngTemplateOutletContext]="{ violation: violation }"></ng-template>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</mat-expansion-panel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mb-2" [hidden]="!showEnforcedViolations()" [class.mb-5]="!showWarningViolations()">
|
||||
<div class="border-b pb-2">
|
||||
<div>
|
||||
Enforced Violations
|
||||
<span>({{ enforcedViolations.length }})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
@for (violation of enforcedViolations; track violation.scope) {
|
||||
<li class="mt-2 pb-2 border-b last-of-type:border-0">
|
||||
<div class="warn-color-darker">{{ getRuleName(violation.ruleId) }}</div>
|
||||
<div class="flex align-center justify-between ml-2">
|
||||
<div class="flex flex-col items-start">
|
||||
<div *ngIf="violation?.subjectPermissionDto?.canRead; else unauthorized">
|
||||
{{ violation.subjectDisplayName }}
|
||||
</div>
|
||||
<span class="text-sm">{{ violation.subjectId }}</span>
|
||||
</div>
|
||||
|
||||
<ng-template [ngTemplateOutlet]="violationMenuTemplate"
|
||||
[ngTemplateOutletContext]="{ violation: violation }"></ng-template>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mb-2" [hidden]="!showWarningViolations()">
|
||||
<div class="border-b pb-2">
|
||||
<div>
|
||||
Warning Violations
|
||||
<span>({{ warningViolations.length }})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
@for (violation of warningViolations; track violation.scope) {
|
||||
<li class="mt-2 pb-2 border-b last-of-type:border-0">
|
||||
<div class="warn-color-darker">{{ getRuleName(violation.ruleId) }}</div>
|
||||
<div class="flex align-center justify-between ml-2">
|
||||
<div class="flex flex-col items-start">
|
||||
<div *ngIf="violation?.subjectPermissionDto?.canRead; else unauthorized">
|
||||
{{ violation.subjectDisplayName }}
|
||||
</div>
|
||||
<span class="text-sm">{{ violation.subjectId }}</span>
|
||||
</div>
|
||||
|
||||
<ng-template
|
||||
[ngTemplateOutlet]="violationMenuTemplate"
|
||||
[ngTemplateOutletContext]="{ violation: violation }"></ng-template>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #violationMenuTemplate let-violation="violation">
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
[matMenuTriggerFor]="violationMenu"
|
||||
class="h-16 w-16 flex items-center justify-center">
|
||||
<i class="fa fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<mat-menu #violationMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="viewViolationDetails(violation)"
|
||||
[disabled]="!violation?.subjectPermissionDto?.canRead">
|
||||
<i class="fa fa-info-circle mr-2 primary-color"></i>Violation details
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[routerLink]="getProcessorLink(violation)"
|
||||
*ngIf="violation?.subjectComponentType === 'PROCESSOR' && violation?.subjectPermissionDto?.canRead">
|
||||
<i class="fa mr-2 fa-long-arrow-right primary-color"></i>Go to component
|
||||
</button>
|
||||
</mat-menu>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #unauthorized> Unauthorized </ng-template>
|
|
@ -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,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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FlowAnalysisDrawerComponent } from './flow-analysis-drawer.component';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('FlowAnalysisDrawerComponent', () => {
|
||||
let component: FlowAnalysisDrawerComponent;
|
||||
let fixture: ComponentFixture<FlowAnalysisDrawerComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FlowAnalysisDrawerComponent, NoopAnimationsModule],
|
||||
providers: [provideMockStore({})]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FlowAnalysisDrawerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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, model } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { navigateToComponentDocumentation } from '../../../../../../state/documentation/documentation.actions';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { selectFlowAnalysisState } from '../../../../state/flow-analysis/flow-analysis.selectors';
|
||||
import {
|
||||
navigateToEditFlowAnalysisRule,
|
||||
startPollingFlowAnalysis,
|
||||
openRuleDetailsDialog
|
||||
} from '../../../../state/flow-analysis/flow-analysis.actions';
|
||||
import { FlowAnalysisRule, FlowAnalysisRuleViolation } from '../../../../state/flow-analysis';
|
||||
import {
|
||||
selectBreadcrumbs,
|
||||
selectCurrentProcessGroupId
|
||||
} from '../../../../state/flow/flow.selectors';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { NifiSpinnerDirective } from '../../../../../../ui/common/spinner/nifi-spinner.directive';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { ComponentContext } from '@nifi/shared';
|
||||
import { BreadcrumbEntity } from '../../../../state/shared';
|
||||
|
||||
@Component({
|
||||
selector: 'flow-analysis-drawer',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatMenuModule,
|
||||
MatIconModule,
|
||||
MatExpansionModule,
|
||||
MatCheckboxModule,
|
||||
FormsModule,
|
||||
RouterLink,
|
||||
NifiSpinnerDirective,
|
||||
MatIconButton,
|
||||
ComponentContext
|
||||
],
|
||||
templateUrl: './flow-analysis-drawer.component.html',
|
||||
styleUrl: './flow-analysis-drawer.component.scss'
|
||||
})
|
||||
export class FlowAnalysisDrawerComponent {
|
||||
violationsMap = new Map();
|
||||
warningRules: FlowAnalysisRule[] = [];
|
||||
enforcedRules: FlowAnalysisRule[] = [];
|
||||
warningViolations: FlowAnalysisRuleViolation[] = [];
|
||||
enforcedViolations: FlowAnalysisRuleViolation[] = [];
|
||||
rules: FlowAnalysisRule[] = [];
|
||||
currentProcessGroupId = '';
|
||||
isAnalysisPending = false;
|
||||
showEnforcedViolations = model(false);
|
||||
showWarningViolations = model(false);
|
||||
flowAnalysisState$ = this.store.select(selectFlowAnalysisState);
|
||||
currentProcessGroupId$ = this.store.select(selectCurrentProcessGroupId);
|
||||
processGroupName = '';
|
||||
|
||||
constructor(private store: Store) {
|
||||
this.store.dispatch(startPollingFlowAnalysis());
|
||||
this.flowAnalysisState$.pipe(takeUntilDestroyed()).subscribe((res) => {
|
||||
this.clearRulesTracking();
|
||||
this.isAnalysisPending = res.flowAnalysisPending;
|
||||
this.rules = res.rules;
|
||||
|
||||
res.rules.forEach((rule: FlowAnalysisRule) => {
|
||||
if (rule.enforcementPolicy === 'WARN') {
|
||||
this.warningRules.push(rule);
|
||||
} else {
|
||||
this.enforcedRules.push(rule);
|
||||
}
|
||||
});
|
||||
res.ruleViolations.forEach((violation: FlowAnalysisRuleViolation) => {
|
||||
if (this.violationsMap.has(violation.ruleId)) {
|
||||
this.violationsMap.get(violation.ruleId).push(violation);
|
||||
} else {
|
||||
this.violationsMap.set(violation.ruleId, [violation]);
|
||||
}
|
||||
});
|
||||
this.enforcedViolations = res.ruleViolations.filter(function (violation: FlowAnalysisRuleViolation) {
|
||||
return violation.enforcementPolicy === 'ENFORCE';
|
||||
});
|
||||
this.warningViolations = res.ruleViolations.filter(function (violation: FlowAnalysisRuleViolation) {
|
||||
return violation.enforcementPolicy === 'WARN';
|
||||
});
|
||||
});
|
||||
this.currentProcessGroupId$.subscribe((pgId) => {
|
||||
this.currentProcessGroupId = pgId;
|
||||
});
|
||||
this.store
|
||||
.select(selectBreadcrumbs)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((breadcrumbs: BreadcrumbEntity) => {
|
||||
this.processGroupName = breadcrumbs.breadcrumb.name;
|
||||
});
|
||||
}
|
||||
|
||||
openRule(rule: FlowAnalysisRule) {
|
||||
this.store.dispatch(
|
||||
navigateToEditFlowAnalysisRule({
|
||||
id: rule.id
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
clearRulesTracking() {
|
||||
this.enforcedRules = [];
|
||||
this.warningRules = [];
|
||||
this.violationsMap.clear();
|
||||
}
|
||||
|
||||
openDocumentation(rule: FlowAnalysisRule) {
|
||||
this.store.dispatch(
|
||||
navigateToComponentDocumentation({
|
||||
request: {
|
||||
backNavigation: {
|
||||
route: ['/process-groups', this.currentProcessGroupId],
|
||||
routeBoundary: ['/documentation'],
|
||||
context: 'Canvas'
|
||||
},
|
||||
parameters: {
|
||||
select: rule.type,
|
||||
group: rule.bundle.group,
|
||||
artifact: rule.bundle.artifact,
|
||||
version: rule.bundle.version
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
viewViolationDetails(violation: FlowAnalysisRuleViolation) {
|
||||
const ruleTest: FlowAnalysisRule = this.rules.find((rule) => rule.id === violation.ruleId)!;
|
||||
this.store.dispatch(openRuleDetailsDialog({ violation, rule: ruleTest }));
|
||||
}
|
||||
|
||||
getProcessorLink(violation: FlowAnalysisRuleViolation): string[] {
|
||||
return ['/process-groups', violation.groupId, violation.subjectComponentType, violation.subjectId];
|
||||
}
|
||||
|
||||
getRuleName(id: string) {
|
||||
const rule = this.rules.find(function (rule: FlowAnalysisRule) {
|
||||
return rule.id === id;
|
||||
});
|
||||
|
||||
return rule ? rule.name : '';
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
@use 'sass:map';
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
@mixin generate-theme($material-theme, $supplemental-theme) {
|
||||
// Get the color config from the theme.
|
||||
$material-theme-color-config: mat.m2-get-color-config($material-theme);
|
||||
$supplemental-theme-color-config: mat.m2-get-color-config($supplemental-theme);
|
||||
|
||||
// Get the color palette from the color-config.
|
||||
$material-theme-primary-palette: map.get($material-theme-color-config, 'primary');
|
||||
$material-theme-warn-palette: map.get($material-theme-color-config, 'warn');
|
||||
$supplemental-theme-surface-palette: map.get($supplemental-theme-color-config, 'primary');
|
||||
|
||||
// Get hues from palette
|
||||
$is-dark: map-get($material-theme-color-config, is-dark);
|
||||
$material-theme-primary-palette-default: mat.m2-get-color-from-palette($material-theme-primary-palette);
|
||||
$material-theme-primary-palette-lighter: mat.m2-get-color-from-palette($material-theme-primary-palette, lighter);
|
||||
$material-theme-warn-palette-darker: mat.m2-get-color-from-palette($material-theme-warn-palette, darker);
|
||||
$supplemental-theme-surface-palette-darker-contrast: mat.m2-get-color-from-palette(
|
||||
$supplemental-theme-surface-palette,
|
||||
darker-contrast
|
||||
);
|
||||
$supplemental-theme-surface-palette-lighter-contrast: mat.m2-get-color-from-palette(
|
||||
$supplemental-theme-surface-palette,
|
||||
lighter-contrast
|
||||
);
|
||||
|
||||
.pill.enforce {
|
||||
background-color: $material-theme-warn-palette-darker;
|
||||
color: if(
|
||||
$is-dark,
|
||||
$supplemental-theme-surface-palette-lighter-contrast,
|
||||
$supplemental-theme-surface-palette-darker-contrast
|
||||
);
|
||||
}
|
||||
|
||||
.pill.warn {
|
||||
background-color: if(
|
||||
$is-dark,
|
||||
$material-theme-primary-palette-lighter,
|
||||
$material-theme-primary-palette-default
|
||||
);
|
||||
color: if(
|
||||
$is-dark,
|
||||
$supplemental-theme-surface-palette-lighter-contrast,
|
||||
$supplemental-theme-surface-palette-darker-contrast
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title>
|
||||
<div class="flex justify-between items-baseline">
|
||||
<div>Violation Information</div>
|
||||
</div>
|
||||
</h2>
|
||||
<mat-dialog-content>
|
||||
<div>
|
||||
<div class="flex justify-between mt-4 mb-4">
|
||||
<div>Violation</div>
|
||||
<div class="py-1 px-2 rounded-xl pill" [ngClass]="violation.enforcementPolicy | lowercase">
|
||||
{{ violation.enforcementPolicy }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>{{ violation.violationMessage }}</p>
|
||||
|
||||
<button mat-menu-item (click)="viewDocumentation()">
|
||||
<i class="fa fa-book primary-color mr-2"></i>
|
||||
View Documentation
|
||||
</button>
|
||||
</div>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button mat-dialog-close color="primary">Close</button>
|
||||
</mat-dialog-actions>
|
||||
</mat-dialog-content>
|
|
@ -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,104 @@
|
|||
/*
|
||||
* 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 { ViolationDetailsDialogComponent } from './violation-details-dialog.component';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
|
||||
describe('ViolationDetailsDialogComponent', () => {
|
||||
let component: ViolationDetailsDialogComponent;
|
||||
let fixture: ComponentFixture<ViolationDetailsDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ViolationDetailsDialogComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {
|
||||
violation: {
|
||||
enforcementPolicy: 'ENFORCE',
|
||||
scope: '4b4cfdf2-0190-1000-500d-b170055a0a34',
|
||||
subjectId: '4b4cfdf2-0190-1000-500d-b170055a0a34',
|
||||
subjectDisplayName: 'AttributeRollingWindow',
|
||||
groupId: '36890551-0190-1000-cfaa-27b854604d18',
|
||||
ruleId: '369c8d4e-0190-1000-99b5-8c4e0144f1a9',
|
||||
issueId: 'default',
|
||||
violationMessage: "'AttributeRollingWindow' is not allowed",
|
||||
subjectComponentType: 'PROCESSOR',
|
||||
subjectPermissionDto: {
|
||||
canRead: true,
|
||||
canWrite: true
|
||||
},
|
||||
enabled: false
|
||||
},
|
||||
rule: {
|
||||
id: '369c8d4e-0190-1000-99b5-8c4e0144f1a9',
|
||||
name: 'DisallowComponentType',
|
||||
type: 'org.apache.nifi.flowanalysis.rules.DisallowComponentType',
|
||||
bundle: {
|
||||
group: 'org.apache.nifi',
|
||||
artifact: 'nifi-standard-nar',
|
||||
version: '2.0.0-SNAPSHOT'
|
||||
},
|
||||
state: 'ENABLED',
|
||||
persistsState: false,
|
||||
restricted: false,
|
||||
deprecated: false,
|
||||
multipleVersionsAvailable: false,
|
||||
supportsSensitiveDynamicProperties: false,
|
||||
enforcementPolicy: 'ENFORCE',
|
||||
properties: {
|
||||
'component-type': 'AttributeRollingWindow'
|
||||
},
|
||||
descriptors: {
|
||||
'component-type': {
|
||||
name: 'component-type',
|
||||
displayName: 'Component Type',
|
||||
description:
|
||||
"Components of the given type will produce a rule violation (i.e. they shouldn't exist). Either the simple or the fully qualified name of the type should be provided.",
|
||||
required: true,
|
||||
sensitive: false,
|
||||
dynamic: false,
|
||||
supportsEl: false,
|
||||
expressionLanguageScope: 'Not Supported',
|
||||
dependencies: []
|
||||
}
|
||||
},
|
||||
validationStatus: 'VALID',
|
||||
extensionMissing: false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
provideMockStore({})
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ViolationDetailsDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { CommonModule } from '@angular/common';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { FlowAnalysisRule, FlowAnalysisRuleViolation } from '../../../../../state/flow-analysis';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { navigateToComponentDocumentation } from 'apps/nifi/src/app/state/documentation/documentation.actions';
|
||||
import { selectCurrentProcessGroupId } from '../../../../../state/flow/flow.selectors';
|
||||
|
||||
interface Data {
|
||||
violation: FlowAnalysisRuleViolation;
|
||||
rule: FlowAnalysisRule;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-violation-details-dialog',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatDialogModule],
|
||||
templateUrl: './violation-details-dialog.component.html',
|
||||
styleUrl: './violation-details-dialog.component.scss'
|
||||
})
|
||||
export class ViolationDetailsDialogComponent {
|
||||
violation: FlowAnalysisRuleViolation;
|
||||
rule: FlowAnalysisRule;
|
||||
currentProcessGroupId$ = this.store.select(selectCurrentProcessGroupId);
|
||||
currentProcessGroupId = '';
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: Data,
|
||||
private store: Store
|
||||
) {
|
||||
this.violation = data.violation;
|
||||
this.rule = data.rule;
|
||||
this.currentProcessGroupId$.subscribe((pgId) => {
|
||||
this.currentProcessGroupId = pgId;
|
||||
});
|
||||
}
|
||||
|
||||
viewDocumentation() {
|
||||
this.store.dispatch(
|
||||
navigateToComponentDocumentation({
|
||||
request: {
|
||||
backNavigation: {
|
||||
route: ['/process-groups', this.currentProcessGroupId],
|
||||
routeBoundary: ['/documentation'],
|
||||
context: 'Canvas'
|
||||
},
|
||||
parameters: {
|
||||
select: this.rule.type,
|
||||
group: this.rule.bundle.group,
|
||||
artifact: this.rule.bundle.artifact,
|
||||
version: this.rule.bundle.version
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -77,5 +77,46 @@
|
|||
.controller-bulletins.has-bulletins {
|
||||
background-color: $material-theme-warn-palette-darker;
|
||||
}
|
||||
|
||||
.flow-analysis-notifications.warn {
|
||||
background-color: if(
|
||||
$is-dark,
|
||||
$material-theme-primary-palette-lighter,
|
||||
$material-theme-primary-palette-default
|
||||
);
|
||||
border-right-color: if(
|
||||
$is-dark,
|
||||
$supplemental-theme-surface-palette-lighter-contrast,
|
||||
$supplemental-theme-surface-palette-darker-contrast
|
||||
);
|
||||
|
||||
|
||||
.fa {
|
||||
// invert the contrast colors since the surface is dark in light mode and light in dark mode
|
||||
color: if(
|
||||
$is-dark,
|
||||
$supplemental-theme-surface-palette-lighter-contrast,
|
||||
$supplemental-theme-surface-palette-darker-contrast
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.flow-analysis-notifications.enforce {
|
||||
background-color: $material-theme-warn-palette-darker;
|
||||
border-right-color: if(
|
||||
$is-dark,
|
||||
$supplemental-theme-surface-palette-lighter-contrast,
|
||||
$supplemental-theme-surface-palette-darker-contrast
|
||||
);
|
||||
|
||||
.fa {
|
||||
// invert the contrast colors since the surface is dark in light mode and light in dark mode
|
||||
color: if(
|
||||
$is-dark,
|
||||
$supplemental-theme-surface-palette-lighter-contrast,
|
||||
$supplemental-theme-surface-palette-darker-contrast
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,6 +127,12 @@
|
|||
</div>
|
||||
<div class="flex">
|
||||
<search [currentProcessGroupId]="currentProcessGroupId"></search>
|
||||
<button
|
||||
class="flow-analysis-notifications w-8 border-l border-r flex justify-center items-center pointer"
|
||||
(click)="toggleFlowAnalysis()"
|
||||
[ngClass]="flowAnalysisNotificationClass">
|
||||
<i class="fa fa-medkit"></i>
|
||||
</button>
|
||||
@if (hasBulletins()) {
|
||||
<button
|
||||
nifiTooltip
|
||||
|
|
|
@ -18,11 +18,6 @@
|
|||
.flow-status {
|
||||
box-sizing: content-box;
|
||||
|
||||
.fa,
|
||||
.icon {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.controller-bulletins {
|
||||
cursor: default;
|
||||
}
|
||||
|
|
|
@ -22,23 +22,44 @@ import { BulletinsTip } from '../../../../../../ui/common/tooltips/bulletins-tip
|
|||
import { BulletinEntity, BulletinsTipInput } from '../../../../../../state/shared';
|
||||
|
||||
import { Search } from '../search/search.component';
|
||||
import { NifiTooltipDirective } from '@nifi/shared';
|
||||
import { NifiTooltipDirective, Storage } from '@nifi/shared';
|
||||
import { ClusterSummary } from '../../../../../../state/cluster-summary';
|
||||
import { ConnectedPosition } from '@angular/cdk/overlay';
|
||||
import { FlowAnalysisState } from '../../../../state/flow-analysis';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../../../../state';
|
||||
import { setFlowAnalysisOpen } from '../../../../state/flow/flow.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'flow-status',
|
||||
standalone: true,
|
||||
templateUrl: './flow-status.component.html',
|
||||
imports: [Search, NifiTooltipDirective],
|
||||
imports: [Search, NifiTooltipDirective, CommonModule],
|
||||
styleUrls: ['./flow-status.component.scss']
|
||||
})
|
||||
export class FlowStatus {
|
||||
private static readonly FLOW_ANALYSIS_VISIBILITY_KEY: string = 'flow-analysis-visibility';
|
||||
private static readonly FLOW_ANALYSIS_KEY: string = 'flow-analysis';
|
||||
public flowAnalysisNotificationClass: string = '';
|
||||
@Input() controllerStatus: ControllerStatus = initialState.flowStatus.controllerStatus;
|
||||
@Input() lastRefreshed: string = initialState.flow.processGroupFlow.lastRefreshed;
|
||||
@Input() clusterSummary: ClusterSummary | null = null;
|
||||
@Input() currentProcessGroupId: string = initialState.id;
|
||||
@Input() loadingStatus = false;
|
||||
@Input() flowAnalysisOpen = initialState.flowAnalysisOpen;
|
||||
@Input() set flowAnalysisState(state: FlowAnalysisState) {
|
||||
if (!state.ruleViolations.length) {
|
||||
this.flowAnalysisNotificationClass = 'primary-color';
|
||||
} else {
|
||||
const isEnforcedRuleViolated = state.ruleViolations.find((v) => {
|
||||
return v.enforcementPolicy === 'ENFORCE';
|
||||
});
|
||||
isEnforcedRuleViolated
|
||||
? (this.flowAnalysisNotificationClass = 'enforce')
|
||||
: (this.flowAnalysisNotificationClass = 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
@Input() set bulletins(bulletins: BulletinEntity[]) {
|
||||
if (bulletins) {
|
||||
|
@ -52,6 +73,23 @@ export class FlowStatus {
|
|||
|
||||
protected readonly BulletinsTip = BulletinsTip;
|
||||
|
||||
constructor(
|
||||
private store: Store<NiFiState>,
|
||||
private storage: Storage
|
||||
) {
|
||||
try {
|
||||
const item: { [key: string]: boolean } | null = this.storage.getItem(
|
||||
FlowStatus.FLOW_ANALYSIS_VISIBILITY_KEY
|
||||
);
|
||||
if (item) {
|
||||
const flowAnalysisOpen = item[FlowStatus.FLOW_ANALYSIS_KEY] === true;
|
||||
this.store.dispatch(setFlowAnalysisOpen({ flowAnalysisOpen }));
|
||||
}
|
||||
} catch (e) {
|
||||
// likely could not parse item... ignoring
|
||||
}
|
||||
}
|
||||
|
||||
hasTerminatedThreads(): boolean {
|
||||
return this.controllerStatus.terminatedThreadCount > 0;
|
||||
}
|
||||
|
@ -141,4 +179,18 @@ export class FlowStatus {
|
|||
offsetY: 8
|
||||
};
|
||||
}
|
||||
|
||||
toggleFlowAnalysis(): void {
|
||||
const flowAnalysisOpen = !this.flowAnalysisOpen;
|
||||
this.store.dispatch(setFlowAnalysisOpen({ flowAnalysisOpen }));
|
||||
|
||||
// update the current value in storage
|
||||
let item: { [key: string]: boolean } | null = this.storage.getItem(FlowStatus.FLOW_ANALYSIS_VISIBILITY_KEY);
|
||||
if (item == null) {
|
||||
item = {};
|
||||
}
|
||||
|
||||
item[FlowStatus.FLOW_ANALYSIS_KEY] = flowAnalysisOpen;
|
||||
this.storage.setItem(FlowStatus.FLOW_ANALYSIS_VISIBILITY_KEY, item);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,11 +69,13 @@
|
|||
}
|
||||
</navigation>
|
||||
<flow-status
|
||||
[flowAnalysisOpen]="(flowAnalysisOpen$ | async)!"
|
||||
[controllerStatus]="(controllerStatus$ | async)!"
|
||||
[lastRefreshed]="(lastRefreshed$ | async)!"
|
||||
[clusterSummary]="(clusterSummary$ | async)!"
|
||||
[bulletins]="(controllerBulletins$ | async)!"
|
||||
[currentProcessGroupId]="(currentProcessGroupId$ | async)!"
|
||||
[loadingStatus]="(loadingService.status$ | async)!">
|
||||
[loadingStatus]="(loadingService.status$ | async)!"
|
||||
[flowAnalysisState]="(flowAnalysisState$ | async)!">
|
||||
</flow-status>
|
||||
</header>
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
selectControllerBulletins,
|
||||
selectControllerStatus,
|
||||
selectCurrentProcessGroupId,
|
||||
selectFlowAnalysisOpen,
|
||||
selectLastRefreshed
|
||||
} from '../../../state/flow/flow.selectors';
|
||||
import { LoadingService } from '../../../../../service/loading.service';
|
||||
|
@ -36,6 +37,7 @@ import { RouterLink } from '@angular/router';
|
|||
import { FlowStatus } from './flow-status/flow-status.component';
|
||||
import { Navigation } from '../../../../../ui/common/navigation/navigation.component';
|
||||
import { selectClusterSummary } from '../../../../../state/cluster-summary/cluster-summary.selectors';
|
||||
import { selectFlowAnalysisState } from '../../../state/flow-analysis/flow-analysis.selectors';
|
||||
|
||||
@Component({
|
||||
selector: 'fd-header',
|
||||
|
@ -63,6 +65,8 @@ export class HeaderComponent {
|
|||
controllerBulletins$ = this.store.select(selectControllerBulletins);
|
||||
currentProcessGroupId$ = this.store.select(selectCurrentProcessGroupId);
|
||||
canvasPermissions$ = this.store.select(selectCanvasPermissions);
|
||||
flowAnalysisState$ = this.store.select(selectFlowAnalysisState);
|
||||
flowAnalysisOpen$ = this.store.select(selectFlowAnalysisOpen);
|
||||
|
||||
constructor(
|
||||
private store: Store<CanvasState>,
|
||||
|
|
|
@ -34,6 +34,8 @@
|
|||
@use 'app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component-theme' as flow-status;
|
||||
@use 'app/pages/flow-designer/ui/canvas/header/new-canvas-item/new-canvas-item.component-theme' as new-canvas-item;
|
||||
@use 'app/pages/flow-designer/ui/canvas/header/search/search.component-theme' as search;
|
||||
@use 'app/pages/flow-designer/ui/canvas/header/flow-analysis-drawer/violation-details-dialog/violation-details-dialog.component-theme'
|
||||
as violation-details-dialog;
|
||||
@use 'app/pages/login/feature/login.component-theme' as login;
|
||||
@use 'app/pages/logout/feature/logout.component-theme' as logout;
|
||||
@use 'app/pages/provenance/feature/provenance.component-theme' as provenance;
|
||||
|
@ -107,6 +109,7 @@
|
|||
@include processor-status-table.generate-theme($supplemental-theme-light);
|
||||
@include change-color-dialog.generate-theme($supplemental-theme-light);
|
||||
@include text-editor.generate-theme($material-theme-light, $supplemental-theme-light);
|
||||
@include violation-details-dialog.generate-theme($material-theme-light, $supplemental-theme-light);
|
||||
|
||||
.dark-theme {
|
||||
// Include the dark theme color styles.
|
||||
|
@ -140,4 +143,5 @@
|
|||
@include processor-status-table.generate-theme($supplemental-theme-dark);
|
||||
@include change-color-dialog.generate-theme($supplemental-theme-dark);
|
||||
@include text-editor.generate-theme($material-theme-dark, $supplemental-theme-dark);
|
||||
@include violation-details-dialog.generate-theme($material-theme-dark, $supplemental-theme-dark);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue