NIFI-12597: Introducing a common navigation bar across all pages (#8237)

* NIFI-12597:
- Introducing a common navigation bar across all pages.

* NIFI-12597:
- Introducing navigation bar to queue listing.

This closes #8237
This commit is contained in:
Matt Gilman 2024-01-16 08:48:22 -05:00 committed by GitHub
parent da7c9bcddb
commit 7c09aabb4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 546 additions and 358 deletions

View File

@ -15,14 +15,14 @@
~ limitations under the License.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<div class="pb-5 flex flex-col h-screen justify-between gap-y-5">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="px-5">
<h3 class="text-xl bold access-policies-header">Access Policies</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1">
<div class="px-5 flex-1">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -21,15 +21,23 @@ import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../state/access-policy/access-policy.reducer';
import { Component } from '@angular/core';
describe('AccessPolicies', () => {
let component: AccessPolicies;
let fixture: ComponentFixture<AccessPolicies>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AccessPolicies],
imports: [RouterModule, RouterTestingModule],
imports: [RouterModule, RouterTestingModule, MockNavigation],
providers: [
provideMockStore({
initialState

View File

@ -26,6 +26,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { AccessPolicyEffects } from '../state/access-policy/access-policy.effects';
import { TenantsEffects } from '../state/tenants/tenants.effects';
import { PolicyComponentEffects } from '../state/policy-component/policy-component.effects';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
@NgModule({
declarations: [AccessPolicies],
@ -35,7 +36,8 @@ import { PolicyComponentEffects } from '../state/policy-component/policy-compone
AccessPoliciesRoutingModule,
StoreModule.forFeature(accessPoliciesFeatureKey, reducers),
EffectsModule.forFeature(AccessPolicyEffects, TenantsEffects, PolicyComponentEffects),
MatDialogModule
MatDialogModule,
Navigation
]
})
export class AccessPoliciesModule {}

View File

@ -15,14 +15,12 @@
~ limitations under the License.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<div class="pb-5 flex flex-col h-screen justify-between gap-y-5">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="px-5 flex-1 flex flex-col">
<h3 class="text-xl bold bulletin-board-header">NiFi Bulletin Board</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1">
<bulletin-board></bulletin-board>
<bulletin-board class="flex-1"></bulletin-board>
</div>
</div>

View File

@ -25,6 +25,7 @@ import { BulletinBoardEffects } from '../state/bulletin-board/bulletin-board.eff
import { BulletinsRoutingModule } from './bulletins-routing.module';
import { CounterListingModule } from '../../counters/ui/counter-listing/counter-listing.module';
import { BulletinBoard } from '../ui/bulletin-board/bulletin-board.component';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
@NgModule({
declarations: [Bulletins],
@ -35,7 +36,8 @@ import { BulletinBoard } from '../ui/bulletin-board/bulletin-board.component';
StoreModule.forFeature(bulletinsFeatureKey, reducers),
EffectsModule.forFeature(BulletinBoardEffects),
CounterListingModule,
BulletinBoard
BulletinBoard,
Navigation
]
})
export class BulletinsModule {}

View File

@ -15,14 +15,12 @@
~ limitations under the License.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<div class="pb-5 flex flex-col h-screen justify-between gap-y-5">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="px-5 flex-1 flex flex-col">
<h3 class="text-xl bold counter-header">NiFi Counters</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1">
<counter-listing></counter-listing>
<counter-listing class="flex-1"></counter-listing>
</div>
</div>

View File

@ -22,15 +22,23 @@ import { initialState } from '../state/counter-listing/counter-listing.reducer';
import { CounterListing } from '../ui/counter-listing/counter-listing.component';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
describe('Counters', () => {
let component: Counters;
let fixture: ComponentFixture<Counters>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Counters, CounterListing],
imports: [RouterModule, RouterTestingModule],
imports: [RouterModule, RouterTestingModule, MockNavigation],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(Counters);

View File

@ -25,6 +25,7 @@ import { EffectsModule } from '@ngrx/effects';
import { CounterListingEffects } from '../state/counter-listing/counter-listing.effects';
import { CounterListingModule } from '../ui/counter-listing/counter-listing.module';
import { MatDialogModule } from '@angular/material/dialog';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
@NgModule({
declarations: [Counters],
@ -35,7 +36,8 @@ import { MatDialogModule } from '@angular/material/dialog';
StoreModule.forFeature(countersFeatureKey, reducers),
EffectsModule.forFeature(CounterListingEffects),
CounterListingModule,
MatDialogModule
MatDialogModule,
Navigation
]
})
export class CountersModule {}

View File

@ -18,9 +18,6 @@
.flow-status {
border-bottom: 1px solid #aabbc3;
box-sizing: content-box;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
position: relative;
z-index: 2;
font-size: 15px;
.fa,

View File

@ -15,151 +15,37 @@
~ limitations under the License.
-->
<header>
<nav class="bg-nifi-primary nifi-header">
<div class="flex justify-between items-center h-16 pl-4">
<div class="flex">
<div class="h-16 w-28 mr-6 relative">
<img ngSrc="assets/icons/nifi-logo.svg" fill priority alt="NiFi Logo" />
</div>
<new-canvas-item
[type]="ComponentType.Processor"
iconClass="icon-processor"
iconHoverClass="icon-processor-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.InputPort"
iconClass="icon-port-in"
iconHoverClass="icon-port-in-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.OutputPort"
iconClass="icon-port-out"
iconHoverClass="icon-port-out-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.ProcessGroup"
iconClass="icon-group"
iconHoverClass="icon-group-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.RemoteProcessGroup"
iconClass="icon-group-remote"
iconHoverClass="icon-group-remote-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.Funnel"
iconClass="icon-funnel"
iconHoverClass="icon-funnel-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.Label"
iconClass="icon-label"
iconHoverClass="icon-label-add"></new-canvas-item>
</div>
<div class="flex justify-between items-center gap-x-1" *ngIf="currentUser$ | async as user">
<div class="flex flex-col justify-between items-end gap-y-1">
<div class="current-user">{{ user.identity }}</div>
<a href="#" *ngIf="allowLogin(user)">log in</a>
<a (click)="logout()" *ngIf="hasToken()">log out</a>
</div>
<button
mat-button
[matMenuTriggerFor]="globalMenu"
class="h-16 w-16 flex items-center justify-center icon global-menu">
<i class="fa fa-navicon"></i>
</button>
<mat-menu #globalMenu="matMenu" xPosition="before">
<button mat-menu-item class="global-menu-item" [routerLink]="['/summary']">
<i class="fa fa-fw fa-table mr-2"></i>
Summary
</button>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/counters']"
[disabled]="!user.countersPermissions.canRead">
<i class="icon fa-fw icon-counter mr-2"></i>
Counter
</button>
<button mat-menu-item class="global-menu-item" [routerLink]="['/bulletins']">
<i class="fa fa-fw fa-sticky-note-o mr-2"></i>
Bulletin Board
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/provenance']"
[disabled]="!user.provenancePermissions.canRead">
<i class="icon fa-fw icon-provenance mr-2"></i>
Data Provenance
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/settings']"
[disabled]="!user.controllerPermissions.canRead">
<i class="fa fa-fw fa-wrench mr-2"></i>
Controller Settings
</button>
<button mat-menu-item class="global-menu-item" [routerLink]="['/parameter-contexts']">
<i class="fa fa-fw mr-2"></i>
Parameter Contexts
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-cubes mr-2"></i>
Cluster
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-history mr-2"></i>
Flow Configuration History
</button>
<button mat-menu-item class="global-menu-item" (click)="viewNodeStatusHistory()">
<i class="fa fa-fw fa-area-chart mr-2"></i>
Node Status History
</button>
<button
mat-menu-item
class="global-menu-item"
[disabled]="!user.systemPermissions.canRead"
(click)="viewSystemDiagnostics()">
<i class="fa fa-fw mr-2"></i>
System Diagnostics
</button>
<ng-container *ngIf="flowConfiguration$ | async as flowConfiguration">
<ng-container *ngIf="flowConfiguration.supportsManagedAuthorizer">
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/users']"
[disabled]="!user.tenantsPermissions.canRead">
<i class="fa fa-fw fa-users mr-2"></i>
Users
</button>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/access-policies', 'global']"
[disabled]="
!user.tenantsPermissions.canRead ||
!user.policiesPermissions.canRead ||
!user.policiesPermissions.canWrite
">
<i class="fa fa-fw fa-key mr-2"></i>
Policies
</button>
</ng-container>
</ng-container>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-question-circle mr-2"></i>
Help
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-info-circle mr-2"></i>
About
</button>
</mat-menu>
</div>
</div>
</nav>
<header class="nifi-header">
<navigation>
<new-canvas-item
[type]="ComponentType.Processor"
iconClass="icon-processor"
iconHoverClass="icon-processor-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.InputPort"
iconClass="icon-port-in"
iconHoverClass="icon-port-in-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.OutputPort"
iconClass="icon-port-out"
iconHoverClass="icon-port-out-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.ProcessGroup"
iconClass="icon-group"
iconHoverClass="icon-group-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.RemoteProcessGroup"
iconClass="icon-group-remote"
iconHoverClass="icon-group-remote-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.Funnel"
iconClass="icon-funnel"
iconHoverClass="icon-funnel-add"></new-canvas-item>
<new-canvas-item
[type]="ComponentType.Label"
iconClass="icon-label"
iconHoverClass="icon-label-add"></new-canvas-item>
</navigation>
<flow-status
[controllerStatus]="(controllerStatus$ | async)!"
[lastRefreshed]="(lastRefreshed$ | async)!"

View File

@ -14,50 +14,3 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.nifi-header {
position: relative;
z-index: 3;
.icon {
font-size: 32px;
color: #004849;
}
.current-user {
font-family: 'Roboto Slab';
font-style: normal;
font-weight: normal;
font-size: 12px;
max-width: 250px;
text-overflow: ellipsis;
line-height: normal;
overflow: hidden;
white-space: nowrap;
color: #262626;
}
a {
font-size: 12px;
text-transform: uppercase;
}
.global-menu {
height: 64px;
width: 64px;
border-radius: 0;
}
.global-menu:hover {
background-color: #e3e8eb;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
}
}
button.global-menu-item {
.fa,
.icon {
text-align: start;
color: #728e9b;
}
}

View File

@ -24,27 +24,35 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NewCanvasItem } from './new-canvas-item/new-canvas-item.component';
import { MatMenuModule } from '@angular/material/menu';
import { MatDividerModule } from '@angular/material/divider';
import { FlowStatus } from './flow-status/flow-status.component';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import {
selectClusterSummary,
selectControllerBulletins,
selectControllerStatus
} from '../../../state/flow/flow.selectors';
import { ClusterSummary, ControllerStatus } from '../../../state/flow';
import { Search } from './search/search.component';
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { selectCurrentUser } from '../../../../../state/current-user/current-user.selectors';
import * as fromUser from '../../../../../state/current-user/current-user.reducer';
import { selectFlowConfiguration } from '../../../../../state/flow-configuration/flow-configuration.selectors';
import * as fromFlowConfiguration from '../../../../../state/flow-configuration/flow-configuration.reducer';
import { Component } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
@Component({
selector: 'flow-status',
standalone: true,
template: ''
})
class MockFlowStatus {}
const clusterSummary: ClusterSummary = {
clustered: false,
connectedToCluster: false,
@ -76,17 +84,16 @@ describe('HeaderComponent', () => {
imports: [
HeaderComponent,
NewCanvasItem,
FlowStatus,
Search,
HttpClientTestingModule,
MockFlowStatus,
MatMenuModule,
MatDividerModule,
RouterModule,
RouterTestingModule,
CdkOverlayOrigin,
CdkConnectedOverlay,
FormsModule,
ReactiveFormsModule
ReactiveFormsModule,
MockNavigation
],
providers: [
provideMockStore({
@ -103,14 +110,6 @@ describe('HeaderComponent', () => {
{
selector: selectControllerBulletins,
value: []
},
{
selector: selectCurrentUser,
value: fromUser.initialState.user
},
{
selector: selectFlowConfiguration,
value: fromFlowConfiguration.initialState.flowConfiguration
}
]
})

View File

@ -26,10 +26,6 @@ import {
selectCurrentProcessGroupId,
selectLastRefreshed
} from '../../../state/flow/flow.selectors';
import { selectCurrentUser } from '../../../../../state/current-user/current-user.selectors';
import { CurrentUser } from '../../../../../state/current-user';
import { AuthStorage } from '../../../../../service/auth-storage.service';
import { AuthService } from '../../../../../service/auth.service';
import { LoadingService } from '../../../../../service/loading.service';
import { NewCanvasItem } from './new-canvas-item/new-canvas-item.component';
import { MatButtonModule } from '@angular/material/button';
@ -38,9 +34,7 @@ import { AsyncPipe, NgIf, NgOptimizedImage } from '@angular/common';
import { MatDividerModule } from '@angular/material/divider';
import { RouterLink } from '@angular/router';
import { FlowStatus } from './flow-status/flow-status.component';
import { getNodeStatusHistoryAndOpenDialog } from '../../../../../state/status-history/status-history.actions';
import { getSystemDiagnosticsAndOpenDialog } from '../../../../../state/system-diagnostics/system-diagnostics.actions';
import { selectFlowConfiguration } from '../../../../../state/flow-configuration/flow-configuration.selectors';
import { Navigation } from '../../../../../ui/common/navigation/navigation.component';
@Component({
selector: 'fd-header',
@ -55,7 +49,8 @@ import { selectFlowConfiguration } from '../../../../../state/flow-configuration
RouterLink,
NgIf,
FlowStatus,
NgOptimizedImage
NgOptimizedImage,
Navigation
],
styleUrls: ['./header.component.scss']
})
@ -66,46 +61,10 @@ export class HeaderComponent {
lastRefreshed$ = this.store.select(selectLastRefreshed);
clusterSummary$ = this.store.select(selectClusterSummary);
controllerBulletins$ = this.store.select(selectControllerBulletins);
currentUser$ = this.store.select(selectCurrentUser);
flowConfiguration$ = this.store.select(selectFlowConfiguration);
currentProcessGroupId$ = this.store.select(selectCurrentProcessGroupId);
constructor(
private store: Store<CanvasState>,
private authStorage: AuthStorage,
private authService: AuthService,
public loadingService: LoadingService
) {}
allowLogin(user: CurrentUser): boolean {
return user.anonymous && location.protocol === 'https:';
}
hasToken(): boolean {
return this.authStorage.hasToken();
}
logout(): void {
this.authService.logout();
}
viewNodeStatusHistory(): void {
this.store.dispatch(
getNodeStatusHistoryAndOpenDialog({
request: {
source: 'menu'
}
})
);
}
viewSystemDiagnostics() {
this.store.dispatch(
getSystemDiagnosticsAndOpenDialog({
request: {
nodewise: false
}
})
);
}
}

View File

@ -15,14 +15,12 @@
~ limitations under the License.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<div class="pb-5 flex flex-col h-screen justify-between gap-y-5">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="px-5 flex-1 flex flex-col">
<h3 class="text-xl bold parameter-context-header">Parameter Contexts</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1">
<parameter-context-listing></parameter-context-listing>
<parameter-context-listing class="flex-1"></parameter-context-listing>
</div>
</div>

View File

@ -23,15 +23,23 @@ import { initialState } from '../state/parameter-context-listing/parameter-conte
import { ParameterContextListing } from '../ui/parameter-context-listing/parameter-context-listing.component';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
describe('ParameterContexts', () => {
let component: ParameterContexts;
let fixture: ComponentFixture<ParameterContexts>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ParameterContexts, ParameterContextListing],
imports: [RouterModule, RouterTestingModule],
imports: [RouterModule, RouterTestingModule, MockNavigation],
providers: [
provideMockStore({
initialState

View File

@ -24,6 +24,7 @@ import { ParameterContextsRoutingModule } from './parameter-contexts-routing.mod
import { parameterContextsFeatureKey, reducers } from '../state';
import { ParameterContextListingEffects } from '../state/parameter-context-listing/parameter-context-listing.effects';
import { ParameterContextListingModule } from '../ui/parameter-context-listing/parameter-context-listing.module';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
@NgModule({
declarations: [ParameterContexts],
@ -33,7 +34,8 @@ import { ParameterContextListingModule } from '../ui/parameter-context-listing/p
ParameterContextsRoutingModule,
StoreModule.forFeature(parameterContextsFeatureKey, reducers),
EffectsModule.forFeature(ParameterContextListingEffects),
ParameterContextListingModule
ParameterContextListingModule,
Navigation
]
})
export class ParameterContextsModule {}

View File

@ -15,14 +15,12 @@
~ limitations under the License.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<h3 class="text-xl bold provenance-header">Provenance</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1">
<div class="pb-5 flex flex-col h-screen justify-between gap-y-5">
<header class="nifi-header">
<navigation></navigation>
</header>
<h3 class="px-5 text-xl bold provenance-header">Provenance</h3>
<div class="px-5 flex-1">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -22,15 +22,25 @@ import { provideMockStore } from '@ngrx/store/testing';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { initialState } from '../state/provenance-event-listing/provenance-event-listing.reducer';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { Component } from '@angular/core';
describe('Provenance', () => {
let component: Provenance;
let fixture: ComponentFixture<Provenance>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Provenance],
imports: [RouterModule, RouterTestingModule],
imports: [RouterModule, RouterTestingModule, MockNavigation],
providers: [
provideMockStore({
initialState

View File

@ -25,6 +25,7 @@ import { provenanceFeatureKey, reducers } from '../state';
import { ProvenanceEventListingEffects } from '../state/provenance-event-listing/provenance-event-listing.effects';
import { MatDialogModule } from '@angular/material/dialog';
import { LineageEffects } from '../state/lineage/lineage.effects';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
@NgModule({
declarations: [Provenance],
@ -34,7 +35,8 @@ import { LineageEffects } from '../state/lineage/lineage.effects';
MatDialogModule,
ProvenanceRoutingModule,
StoreModule.forFeature(provenanceFeatureKey, reducers),
EffectsModule.forFeature(ProvenanceEventListingEffects, LineageEffects)
EffectsModule.forFeature(ProvenanceEventListingEffects, LineageEffects),
Navigation
]
})
export class ProvenanceModule {}

View File

@ -15,13 +15,11 @@
~ limitations under the License.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-end">
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1">
<div class="pb-5 flex flex-col h-screen justify-between gap-y-5">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="px-5 flex-1">
<router-outlet></router-outlet>
</div>
</div>

View File

@ -22,15 +22,23 @@ import { provideMockStore } from '@ngrx/store/testing';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { initialState } from '../state/queue-listing/queue-listing.reducer';
import { Component } from '@angular/core';
describe('Queue', () => {
let component: Queue;
let fixture: ComponentFixture<Queue>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Queue],
imports: [RouterModule, RouterTestingModule],
imports: [RouterModule, RouterTestingModule, MockNavigation],
providers: [
provideMockStore({
initialState

View File

@ -17,17 +17,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { Queue } from './queue.component';
import { QueueRoutingModule } from './queue-routing.module';
import { queueFeatureKey, reducers } from '../state';
import { MatDialogModule } from '@angular/material/dialog';
import { QueueListingEffects } from '../state/queue-listing/queue-listing.effects';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
@NgModule({
declarations: [Queue],
exports: [Queue],
imports: [CommonModule, MatDialogModule, QueueRoutingModule]
imports: [CommonModule, MatDialogModule, QueueRoutingModule, Navigation]
})
export class QueueModule {}

View File

@ -16,16 +16,10 @@
*/
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { NiFiCommon } from '../../../service/nifi-common.service';
import { ParameterContextUpdateRequest, SubmitParameterContextUpdate } from '../../../state/shared';
import {
FlowFileSummary,
ListingRequest,
ListingRequestEntity,
SubmitQueueListingRequest
} from '../state/queue-listing';
import { FlowFileSummary, ListingRequest, SubmitQueueListingRequest } from '../state/queue-listing';
@Injectable({ providedIn: 'root' })
export class QueueService {

View File

@ -22,11 +22,9 @@ import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bu
import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
import { NiFiCommon } from '../../../../../service/nifi-common.service';
import { NgForOf, NgIf } from '@angular/common';
import { ProvenanceEventSummary } from '../../../../../state/shared';
import { RouterLink } from '@angular/router';
import { FlowFileSummary, ListingRequest } from '../../../state/queue-listing';
import { CurrentUser } from '../../../../../state/current-user';
import { Flow } from '../../../../flow-designer/state/flow';
@Component({
selector: 'flowfile-table',

View File

@ -15,14 +15,12 @@
~ limitations under the License.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<div class="pb-5 flex flex-col h-screen justify-between gap-y-5">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="px-5 flex-1 flex flex-col">
<h3 class="text-xl bold settings-header">NiFi Settings</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1 flex flex-col">
<div class="settings-tabs">
<nav mat-tab-nav-bar color="primary" [tabPanel]="tabPanel">
<a

View File

@ -23,15 +23,23 @@ import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { initialState } from '../state/general/general.reducer';
import { Component } from '@angular/core';
describe('SettingsComponent', () => {
describe('Settings', () => {
let component: Settings;
let fixture: ComponentFixture<Settings>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Settings],
imports: [MatTabsModule, RouterModule, RouterTestingModule],
imports: [MatTabsModule, RouterModule, RouterTestingModule, MockNavigation],
providers: [
provideMockStore({
initialState

View File

@ -34,6 +34,7 @@ import { MatTabsModule } from '@angular/material/tabs';
import { ReportingTasksEffects } from '../state/reporting-tasks/reporting-tasks.effects';
import { RegistryClientsEffects } from '../state/registry-clients/registry-clients.effects';
import { FlowAnalysisRulesEffects } from '../state/flow-analysis-rules/flow-analysis-rules.effects';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
@NgModule({
declarations: [Settings],
@ -55,7 +56,8 @@ import { FlowAnalysisRulesEffects } from '../state/flow-analysis-rules/flow-anal
FlowAnalysisRulesEffects,
RegistryClientsEffects
),
MatTabsModule
MatTabsModule,
Navigation
]
})
export class SettingsModule {}

View File

@ -15,14 +15,12 @@
~ limitations under the License.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<div class="pb-5 flex flex-col h-screen justify-between gap-y-5">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="px-5 flex-1 flex flex-col">
<h3 class="text-xl bold summary-header">NiFi Summary</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1 flex flex-col">
<div class="summary-tabs">
<nav mat-tab-nav-bar color="primary" [tabPanel]="tabPanel">
<a

View File

@ -22,15 +22,23 @@ import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { Summary } from './summary.component';
import { initialState } from '../state/summary-listing/summary-listing.reducer';
import { Component } from '@angular/core';
describe('Summary', () => {
let component: Summary;
let fixture: ComponentFixture<Summary>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Summary],
imports: [MatTabsModule, RouterModule, RouterTestingModule],
imports: [MatTabsModule, RouterModule, RouterTestingModule, MockNavigation],
providers: [
provideMockStore({
initialState

View File

@ -31,6 +31,7 @@ import { OutputPortStatusListingModule } from '../ui/output-port-status-listing/
import { InputPortStatusListingModule } from '../ui/input-port-status-listing/input-port-status-listing.module';
import { SummaryListingEffects } from '../state/summary-listing/summary-listing.effects';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
@NgModule({
declarations: [Summary],
@ -48,7 +49,8 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
InputPortStatusListingModule,
StoreModule.forFeature(summaryFeatureKey, reducers),
EffectsModule.forFeature(SummaryListingEffects),
NgxSkeletonLoaderModule
NgxSkeletonLoaderModule,
Navigation
]
})
export class SummaryModule {}

View File

@ -15,14 +15,12 @@
~ limitations under the License.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<div class="pb-5 flex flex-col h-screen justify-between gap-y-5">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="px-5 flex-1 flex flex-col">
<h3 class="text-xl bold user-header">NiFi Users</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1">
<user-listing></user-listing>
<user-listing class="flex-1"></user-listing>
</div>
</div>

View File

@ -22,15 +22,23 @@ import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { UserListing } from '../ui/user-listing/user-listing.component';
import { initialState } from '../state/user-listing/user-listing.reducer';
import { Component } from '@angular/core';
describe('Users', () => {
let component: Users;
let fixture: ComponentFixture<Users>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Users, UserListing],
imports: [RouterModule, RouterTestingModule],
imports: [RouterModule, RouterTestingModule, MockNavigation],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(Users);

View File

@ -25,6 +25,7 @@ import { EffectsModule } from '@ngrx/effects';
import { MatDialogModule } from '@angular/material/dialog';
import { UserListingEffects } from '../state/user-listing/user-listing.effects';
import { UserListingModule } from '../ui/user-listing/user-listing.module';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
@NgModule({
declarations: [Users],
@ -35,7 +36,8 @@ import { UserListingModule } from '../ui/user-listing/user-listing.module';
StoreModule.forFeature(usersFeatureKey, reducers),
EffectsModule.forFeature(UserListingEffects),
MatDialogModule,
UserListingModule
UserListingModule,
Navigation
]
})
export class UsersModule {}

View File

@ -0,0 +1,145 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<nav class="bg-nifi-primary nifi-navigation">
<div class="flex justify-between items-center h-16 pl-4">
<div class="flex">
<div class="h-16 w-28 mr-6 relative">
<img
ngSrc="assets/icons/nifi-logo.svg"
fill
priority
alt="NiFi Logo"
class="pointer"
[routerLink]="['/']" />
</div>
<ng-content></ng-content>
</div>
<div class="flex justify-between items-center gap-x-1" *ngIf="currentUser$ | async as user">
<div class="flex flex-col justify-between items-end gap-y-1">
<div class="current-user">{{ user.identity }}</div>
<a href="#" *ngIf="allowLogin(user)">log in</a>
<a (click)="logout()" *ngIf="hasToken()">log out</a>
</div>
<button
mat-button
[matMenuTriggerFor]="globalMenu"
class="h-16 w-16 flex items-center justify-center icon global-menu">
<i class="fa fa-navicon"></i>
</button>
<mat-menu #globalMenu="matMenu" xPosition="before">
<button mat-menu-item class="global-menu-item" [routerLink]="['/']">
<i class="icon icon-drop mr-2"></i>
Canvas
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item" [routerLink]="['/summary']">
<i class="fa fa-fw fa-table mr-2"></i>
Summary
</button>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/counters']"
[disabled]="!user.countersPermissions.canRead">
<i class="icon fa-fw icon-counter mr-2"></i>
Counter
</button>
<button mat-menu-item class="global-menu-item" [routerLink]="['/bulletins']">
<i class="fa fa-fw fa-sticky-note-o mr-2"></i>
Bulletin Board
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/provenance']"
[disabled]="!user.provenancePermissions.canRead">
<i class="icon fa-fw icon-provenance mr-2"></i>
Data Provenance
</button>
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/settings']"
[disabled]="!user.controllerPermissions.canRead">
<i class="fa fa-fw fa-wrench mr-2"></i>
Controller Settings
</button>
<button mat-menu-item class="global-menu-item" [routerLink]="['/parameter-contexts']">
<i class="fa fa-fw mr-2"></i>
Parameter Contexts
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-cubes mr-2"></i>
Cluster
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-history mr-2"></i>
Flow Configuration History
</button>
<button mat-menu-item class="global-menu-item" (click)="viewNodeStatusHistory()">
<i class="fa fa-fw fa-area-chart mr-2"></i>
Node Status History
</button>
<button
mat-menu-item
class="global-menu-item"
[disabled]="!user.systemPermissions.canRead"
(click)="viewSystemDiagnostics()">
<i class="fa fa-fw mr-2"></i>
System Diagnostics
</button>
<ng-container *ngIf="flowConfiguration$ | async as flowConfiguration">
<ng-container *ngIf="flowConfiguration.supportsManagedAuthorizer">
<mat-divider></mat-divider>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/users']"
[disabled]="!user.tenantsPermissions.canRead">
<i class="fa fa-fw fa-users mr-2"></i>
Users
</button>
<button
mat-menu-item
class="global-menu-item"
[routerLink]="['/access-policies', 'global']"
[disabled]="
!user.tenantsPermissions.canRead ||
!user.policiesPermissions.canRead ||
!user.policiesPermissions.canWrite
">
<i class="fa fa-fw fa-key mr-2"></i>
Policies
</button>
<mat-divider></mat-divider>
</ng-container>
</ng-container>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-question-circle mr-2"></i>
Help
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-info-circle mr-2"></i>
About
</button>
</mat-menu>
</div>
</div>
</nav>

View File

@ -0,0 +1,60 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.nifi-navigation {
.icon {
font-size: 32px;
color: #004849;
}
.current-user {
font-family: 'Roboto Slab';
font-style: normal;
font-weight: normal;
font-size: 12px;
max-width: 250px;
text-overflow: ellipsis;
line-height: normal;
overflow: hidden;
white-space: nowrap;
color: #262626;
}
a {
font-size: 12px;
text-transform: uppercase;
}
.global-menu {
height: 64px;
width: 64px;
border-radius: 0;
}
.global-menu:hover {
background-color: #e3e8eb;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
}
}
button.global-menu-item {
.fa,
.icon {
text-align: start;
color: #728e9b;
}
}

View File

@ -0,0 +1,47 @@
/*
* 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 { Navigation } from './navigation.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/current-user/current-user.reducer';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterTestingModule } from '@angular/router/testing';
describe('Navigation', () => {
let component: Navigation;
let fixture: ComponentFixture<Navigation>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [Navigation, HttpClientTestingModule, RouterTestingModule],
providers: [
provideMockStore({
initialState
})
]
});
fixture = TestBed.createComponent(Navigation);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,82 @@
/*
* 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 } from '@angular/core';
import { AsyncPipe, NgIf, NgOptimizedImage } from '@angular/common';
import { MatDividerModule } from '@angular/material/divider';
import { MatMenuModule } from '@angular/material/menu';
import { getNodeStatusHistoryAndOpenDialog } from '../../../state/status-history/status-history.actions';
import { getSystemDiagnosticsAndOpenDialog } from '../../../state/system-diagnostics/system-diagnostics.actions';
import { Store } from '@ngrx/store';
import { AuthStorage } from '../../../service/auth-storage.service';
import { AuthService } from '../../../service/auth.service';
import { CurrentUser } from '../../../state/current-user';
import { RouterLink } from '@angular/router';
import { selectCurrentUser } from '../../../state/current-user/current-user.selectors';
import { MatButtonModule } from '@angular/material/button';
import { NiFiState } from '../../../state';
import { selectFlowConfiguration } from '../../../state/flow-configuration/flow-configuration.selectors';
@Component({
selector: 'navigation',
standalone: true,
imports: [NgOptimizedImage, AsyncPipe, MatDividerModule, MatMenuModule, NgIf, RouterLink, MatButtonModule],
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss']
})
export class Navigation {
currentUser$ = this.store.select(selectCurrentUser);
flowConfiguration$ = this.store.select(selectFlowConfiguration);
constructor(
private store: Store<NiFiState>,
private authStorage: AuthStorage,
private authService: AuthService
) {}
allowLogin(user: CurrentUser): boolean {
return user.anonymous && location.protocol === 'https:';
}
hasToken(): boolean {
return this.authStorage.hasToken();
}
logout(): void {
this.authService.logout();
}
viewNodeStatusHistory(): void {
this.store.dispatch(
getNodeStatusHistoryAndOpenDialog({
request: {
source: 'menu'
}
})
);
}
viewSystemDiagnostics() {
this.store.dispatch(
getSystemDiagnosticsAndOpenDialog({
request: {
nodewise: false
}
})
);
}
}

View File

@ -248,6 +248,11 @@ a:hover {
text-decoration-color: #004849;
}
header.nifi-header {
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
position: relative;
}
/*
Icon for styling mat-icon in forms throughout the application.
*/