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:
Shane Ardell 2024-08-19 09:04:52 -04:00 committed by GitHub
parent 9fbe6aab74
commit 8e0c4aeb33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 1411 additions and 51 deletions

View File

@ -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,

View File

@ -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({

View File

@ -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({

View File

@ -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(() => {

View File

@ -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({

View File

@ -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({

View File

@ -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({

View File

@ -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({

View File

@ -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({

View File

@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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();
});
});

View File

@ -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}`);
}
}

View File

@ -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({

View File

@ -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({

View File

@ -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({

View File

@ -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({

View File

@ -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({

View File

@ -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({

View File

@ -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({

View File

@ -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 }>()
);

View File

@ -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 }
);
}

View File

@ -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
}))
);

View File

@ -0,0 +1,25 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createSelector } from '@ngrx/store';
import { flowAnalysisFeatureKey } from './index';
import { CanvasState, selectCanvasState } from '../index';
export const selectFlowAnalysisState = createSelector(
selectCanvasState,
(state: CanvasState) => state[flowAnalysisFeatureKey]
);

View File

@ -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';
}

View File

@ -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
*/

View File

@ -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,

View File

@ -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,

View File

@ -257,3 +257,5 @@ export const selectMaxZIndex = (componentType: ComponentType.Connection | Compon
);
}
};
export const selectFlowAnalysisOpen = createSelector(selectFlowState, (state: FlowState) => state.flowAnalysisOpen);

View File

@ -638,6 +638,7 @@ export interface FlowState {
saving: boolean;
navigationCollapsed: boolean;
operationCollapsed: boolean;
flowAnalysisOpen: boolean;
versionSaving: boolean;
changeVersionRequest: FlowUpdateRequestEntity | null;
copiedSnippet: CopiedSnippet | null;

View File

@ -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);
}

View File

@ -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 {

View File

@ -18,12 +18,19 @@
<div class="flex flex-col h-full">
<fd-header></fd-header>
<div class="flex-1">
<graph-controls></graph-controls>
<div
id="canvas-container"
class="canvas-background select-none h-full"
[cdkContextMenuTriggerFor]="contextMenu.menu"></div>
<fd-context-menu #contextMenu [menuProvider]="canvasContextMenu" menuId="root"></fd-context-menu>
<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 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>

View File

@ -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);

View File

@ -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,

View File

@ -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 {}

View File

@ -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>

View File

@ -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.
*/

View File

@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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();
});
});

View File

@ -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 : '';
}
}

View File

@ -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
);
}
}

View File

@ -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>

View File

@ -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.
*/

View File

@ -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();
});
});

View File

@ -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
}
}
})
);
}
}

View File

@ -34,42 +34,42 @@
$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-lighter: mat.m2-get-color-from-palette(
$supplemental-theme-surface-palette,
lighter
$supplemental-theme-surface-palette,
lighter
);
$supplemental-theme-surface-palette-darker: mat.m2-get-color-from-palette(
$supplemental-theme-surface-palette,
darker
$supplemental-theme-surface-palette,
darker
);
$supplemental-theme-surface-palette-darker-contrast: mat.m2-get-color-from-palette(
$supplemental-theme-surface-palette,
darker-contrast
$supplemental-theme-surface-palette,
darker-contrast
);
$supplemental-theme-surface-palette-lighter-contrast: mat.m2-get-color-from-palette(
$supplemental-theme-surface-palette,
lighter-contrast
$supplemental-theme-surface-palette,
lighter-contrast
);
.flow-status {
background: if(
$is-dark,
$supplemental-theme-surface-palette-darker,
$supplemental-theme-surface-palette-lighter
$is-dark,
$supplemental-theme-surface-palette-darker,
$supplemental-theme-surface-palette-lighter
);
.controller-bulletins {
background-color: if(
$is-dark,
$material-theme-primary-palette-lighter,
$material-theme-primary-palette-default
$is-dark,
$material-theme-primary-palette-lighter,
$material-theme-primary-palette-default
);
.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
$is-dark,
$supplemental-theme-surface-palette-lighter-contrast,
$supplemental-theme-surface-palette-darker-contrast
);
}
}
@ -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
);
}
}
}
}

View File

@ -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

View File

@ -18,11 +18,6 @@
.flow-status {
box-sizing: content-box;
.fa,
.icon {
font-style: normal;
}
.controller-bulletins {
cursor: default;
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>,

View File

@ -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);
}