NIFI-13373: Adding support for banner text (#8947)

* NIFI-13373:
- Adding support for banner text.

* NIFI-13373:
- Prettier.

* NIFI-13373:
- Removing unused property.

* NIFI-13373:
- Defining reponse payload when loading banner text.
- Removing banner text from login, logout, and error pages.

* NIFI-13373:
- Only loading the banner text when necessary.

This closes #8947
This commit is contained in:
Matt Gilman 2024-06-11 11:47:56 -04:00 committed by GitHub
parent 2ef31f2372
commit b3c7952ef3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 840 additions and 231 deletions

View File

@ -92,6 +92,7 @@ public class MiNiFiPropertiesGenerator {
Triple.of(NiFiProperties.BORED_YIELD_DURATION, "10 millis",
"# If a component has no work to do (is \"bored\"), how long should we wait before checking again for work"),
Triple.of(NiFiProperties.LOGIN_IDENTITY_PROVIDER_CONFIGURATION_FILE, "./conf/login-identity-providers.xml", EMPTY),
Triple.of(NiFiProperties.UI_BANNER_TEXT, EMPTY, EMPTY),
Triple.of(NiFiProperties.NAR_LIBRARY_DIRECTORY, "./lib", EMPTY),
Triple.of(NiFiProperties.NAR_WORKING_DIRECTORY, "./work/nar/", EMPTY),
Triple.of(NiFiProperties.NAR_LIBRARY_AUTOLOAD_DIRECTORY, "./extensions", EMPTY),

View File

@ -223,6 +223,9 @@ public class NiFiProperties extends ApplicationProperties {
public static final String WEB_REQUEST_LOG_FORMAT = "nifi.web.request.log.format";
public static final String WEB_JMX_METRICS_ALLOWED_FILTER_PATTERN = "nifi.web.jmx.metrics.allowed.filter.pattern";
// ui properties
public static final String UI_BANNER_TEXT = "nifi.ui.banner.text";
// cluster common properties
public static final String CLUSTER_PROTOCOL_HEARTBEAT_INTERVAL = "nifi.cluster.protocol.heartbeat.interval";
public static final String CLUSTER_PROTOCOL_HEARTBEAT_MISSABLE_MAX = "nifi.cluster.protocol.heartbeat.missable.max";
@ -803,6 +806,17 @@ public class NiFiProperties extends ApplicationProperties {
return new File(getProperty(NAR_LIBRARY_AUTOLOAD_DIRECTORY, DEFAULT_NAR_LIBRARY_AUTOLOAD_DIR));
}
// getters for ui properties //
/**
* Get the banner text.
*
* @return The banner text
*/
public String getBannerText() {
return this.getProperty(UI_BANNER_TEXT, StringUtils.EMPTY);
}
/**
* Returns true if auto reload of the keystore and truststore is enabled.
*

View File

@ -44,6 +44,8 @@ public class NiFiPropertiesTest {
NiFiProperties properties = loadNiFiProperties("/NiFiProperties/conf/nifi.properties", null);
assertEquals("UI Banner Text", properties.getBannerText());
Set<File> expectedDirectories = new HashSet<>();
expectedDirectories.add(new File("./target/resources/NiFiProperties/lib/"));
expectedDirectories.add(new File("./target/resources/NiFiProperties/lib2/"));

View File

@ -23,6 +23,7 @@ nifi.administrative.yield.duration=30 sec
nifi.reporting.task.configuration.file=./target/reporting-tasks.xml
nifi.controller.service.configuration.file=./target/controller-services.xml
nifi.ui.banner.text=UI Banner Text
nifi.nar.library.directory=./target/resources/NiFiProperties/lib/
nifi.nar.library.directory.alt=./target/resources/NiFiProperties/lib2/
nifi.nar.working.directory=./target/work/nar/

View File

@ -3212,6 +3212,7 @@ This cleanup mechanism takes into account only automatically created archived _f
|`nifi.authorizer.configuration.file`*|This is the location of the file that specifies how authorizers are defined. The default value is `./conf/authorizers.xml`.
|`nifi.login.identity.provider.configuration.file`*|This is the location of the file that specifies how username/password authentication is performed. This file is
only considered if `nifi.security.user.login.identity.provider` is configured with a provider identifier. The default value is `./conf/login-identity-providers.xml`.
|`nifi.ui.banner.text`|This is banner text that may be configured to display at the top of the User Interface. It is blank by default.
|`nifi.nar.library.directory`|The location of the nar library. The default value is `./lib` and probably should be left as is.
|`nifi.restore.directory`|The location that certain providers (e.g. UserGroupProviders) will look for previous configurations to restore from. There is no default value.
+

View File

@ -0,0 +1,63 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.web.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.xml.bind.annotation.XmlType;
/**
* Banners that should appear on the top and bottom of this NiFi.
*/
@XmlType(name = "banners")
public class BannerDTO {
private String headerText;
private String footerText;
/* getters / setters */
/**
* The banner footer text.
*
* @return The footer text
*/
@Schema(description = "The footer text."
)
public String getFooterText() {
return footerText;
}
public void setFooterText(String footerText) {
this.footerText = footerText;
}
/**
* The banner header text.
*
* @return The header text
*/
@Schema(description = "The header text."
)
public String getHeaderText() {
return headerText;
}
public void setHeaderText(String headerText) {
this.headerText = headerText;
}
}

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.
*/
package org.apache.nifi.web.api.entity;
import jakarta.xml.bind.annotation.XmlRootElement;
import org.apache.nifi.web.api.dto.BannerDTO;
/**
* A serialized representation of this class can be placed in the entity body of a request or response to or from the API. This particular entity holds a reference to a BannerDTO.
*/
@XmlRootElement(name = "bannersEntity")
public class BannerEntity extends Entity {
private BannerDTO banners;
/**
* The BannerDTO that is being serialized.
*
* @return The BannerDTO object
*/
public BannerDTO getBanners() {
return banners;
}
public void setBanners(BannerDTO about) {
this.banners = about;
}
}

View File

@ -72,6 +72,7 @@
<nifi.content.viewer.url>../nifi-content-viewer/</nifi.content.viewer.url>
<nifi.restore.directory />
<nifi.ui.banner.text />
<nifi.nar.library.directory>./lib</nifi.nar.library.directory>
<nifi.nar.library.autoload.directory>./extensions</nifi.nar.library.autoload.directory>
<nifi.nar.working.directory>./work/nar/</nifi.nar.working.directory>

View File

@ -31,6 +31,7 @@ nifi.queue.backpressure.size=${nifi.queue.backpressure.size}
nifi.authorizer.configuration.file=${nifi.authorizer.configuration.file}
nifi.login.identity.provider.configuration.file=${nifi.login.identity.provider.configuration.file}
nifi.ui.banner.text=${nifi.ui.banner.text}
nifi.nar.library.directory=${nifi.nar.library.directory}
nifi.nar.library.autoload.directory=${nifi.nar.library.autoload.directory}
nifi.nar.working.directory=${nifi.nar.working.directory}

View File

@ -28,6 +28,8 @@ nifi.bored.yield.duration=10 millis
nifi.authorizer.configuration.file=./target/conf/authorizers.xml
nifi.login.identity.provider.configuration.file=./target/conf/login-identity-providers.xml
nifi.ui.banner.text=dXwnu9mLyPETJrq1||n9e5dk5+HSTBCGOA/Sy6VYzwPw3baeRNvglalA1Pr1PcToyc4/qT6md24YOP4xVz14jd
nifi.ui.banner.text.protected=aes/gcm/256
nifi.nar.library.directory=./target/lib
nifi.nar.working.directory=./target/work/nar/
nifi.documentation.working.directory=./target/work/docs/components
@ -128,7 +130,7 @@ nifi.web.jetty.threads=200
nifi.sensitive.props.key=dQU402Mz4J+t+e18||6+ictR0Nssq3/rR/d8fq5CFAKmpakr9jCyPIJYxG7n6D86gxsu2TRp4M48ugUw==
nifi.sensitive.props.key.protected=aes/gcm/256
nifi.sensitive.props.algorithm=NIFI_PBKDF2_AES_GCM_256
nifi.sensitive.props.additional.keys=
nifi.sensitive.props.additional.keys=nifi.ui.banner.text
nifi.security.keystore=/path/to/keystore.jks
nifi.security.keystoreType=JKS

View File

@ -56,6 +56,7 @@ import org.apache.nifi.web.NiFiServiceFacade;
import org.apache.nifi.web.ResourceNotFoundException;
import org.apache.nifi.web.Revision;
import org.apache.nifi.web.api.dto.AboutDTO;
import org.apache.nifi.web.api.dto.BannerDTO;
import org.apache.nifi.web.api.dto.BulletinBoardDTO;
import org.apache.nifi.web.api.dto.BulletinQueryDTO;
import org.apache.nifi.web.api.dto.ClusterDTO;
@ -75,6 +76,7 @@ import org.apache.nifi.web.api.dto.status.ControllerStatusDTO;
import org.apache.nifi.web.api.entity.AboutEntity;
import org.apache.nifi.web.api.entity.ActionEntity;
import org.apache.nifi.web.api.entity.ActivateControllerServicesEntity;
import org.apache.nifi.web.api.entity.BannerEntity;
import org.apache.nifi.web.api.entity.BulletinBoardEntity;
import org.apache.nifi.web.api.entity.ClusterSearchResultsEntity;
import org.apache.nifi.web.api.entity.ClusterSummaryEntity;
@ -1340,6 +1342,50 @@ public class FlowResource extends ApplicationResource {
return generateOkResponse(entity).build();
}
/**
* Retrieves the banners for this NiFi.
*
* @return A bannerEntity.
*/
@GET
@Consumes(MediaType.WILDCARD)
@Produces(MediaType.APPLICATION_JSON)
@Path("banners")
@Operation(
summary = "Retrieves the banners for this NiFi",
responses = @ApiResponse(content = @Content(schema = @Schema(implementation = BannerEntity.class))),
security = {
@SecurityRequirement(name = "Read - /flow")
}
)
@ApiResponses(
value = {
@ApiResponse(responseCode = "400", description = "NiFi was unable to complete the request because it was invalid. The request should not be retried without modification."),
@ApiResponse(responseCode = "401", description = "Client could not be authenticated."),
@ApiResponse(responseCode = "403", description = "Client is not authorized to make this request."),
@ApiResponse(responseCode = "409", description = "The request was valid but NiFi was not in the appropriate state to process it.")
}
)
public Response getBanners() {
authorizeFlow();
// get the banner from the properties - will come from the NCM when clustered
final String bannerText = getProperties().getBannerText();
// create the DTO
final BannerDTO bannerDTO = new BannerDTO();
bannerDTO.setHeaderText(bannerText);
bannerDTO.setFooterText(bannerText);
// create the response entity
final BannerEntity entity = new BannerEntity();
entity.setBanners(bannerDTO);
// generate the response
return generateOkResponse(entity).build();
}
/**
* Retrieves the types of processors that this NiFi supports.
*

View File

@ -21,6 +21,6 @@
<mat-spinner color="warn"></mat-spinner>
</div>
</div>
} @else {
<router-outlet></router-outlet>
}
<router-outlet></router-outlet>

View File

@ -49,6 +49,7 @@ import { ClusterSummaryEffects } from './state/cluster-summary/cluster-summary.e
import { PropertyVerificationEffects } from './state/property-verification/property-verification.effects';
import { loadingInterceptor } from './service/interceptors/loading.interceptor';
import { LoginConfigurationEffects } from './state/login-configuration/login-configuration.effects';
import { BannerTextEffects } from './state/banner-text/banner-text.effects';
@NgModule({
declarations: [AppComponent],
@ -71,6 +72,7 @@ import { LoginConfigurationEffects } from './state/login-configuration/login-con
CurrentUserEffects,
ExtensionTypesEffects,
AboutEffects,
BannerTextEffects,
FlowConfigurationEffects,
LoginConfigurationEffects,
StatusHistoryEffects,

View File

@ -15,14 +15,16 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="px-5">
<h3 class="primary-color">Access Policies</h3>
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="px-5">
<h3 class="primary-color">Access Policies</h3>
</div>
<div class="pb-5 px-5 flex-1">
<router-outlet></router-outlet>
</div>
</div>
<div class="pb-5 px-5 flex-1">
<router-outlet></router-outlet>
</div>
</div>
</banner-text>

View File

@ -21,6 +21,7 @@ import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { MockComponent } from 'ng-mocks';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('AccessPolicies', () => {
let component: AccessPolicies;
@ -29,7 +30,7 @@ describe('AccessPolicies', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AccessPolicies],
imports: [RouterModule, RouterTestingModule, MockComponent(Navigation)]
imports: [RouterModule, RouterTestingModule, MockComponent(BannerText), MockComponent(Navigation)]
});
fixture = TestBed.createComponent(AccessPolicies);
component = fixture.componentInstance;

View File

@ -27,6 +27,7 @@ import { AccessPolicyEffects } from '../state/access-policy/access-policy.effect
import { TenantsEffects } from '../state/tenants/tenants.effects';
import { PolicyComponentEffects } from '../state/policy-component/policy-component.effects';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [AccessPolicies],
@ -37,7 +38,8 @@ import { Navigation } from '../../../ui/common/navigation/navigation.component';
StoreModule.forFeature(accessPoliciesFeatureKey, reducers),
EffectsModule.forFeature(AccessPolicyEffects, TenantsEffects, PolicyComponentEffects),
MatDialogModule,
Navigation
Navigation,
BannerText
]
})
export class AccessPoliciesModule {}

View File

@ -15,12 +15,14 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">NiFi Bulletin Board</h3>
<bulletin-board class="flex-1"></bulletin-board>
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">NiFi Bulletin Board</h3>
<bulletin-board class="flex-1"></bulletin-board>
</div>
</div>
</div>
</banner-text>

View File

@ -22,6 +22,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { BulletinBoard } from '../ui/bulletin-board/bulletin-board.component';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { MockComponent } from 'ng-mocks';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('Bulletins', () => {
let component: Bulletins;
@ -30,7 +31,13 @@ describe('Bulletins', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Bulletins],
imports: [RouterModule, RouterTestingModule, MockComponent(Navigation), MockComponent(BulletinBoard)]
imports: [
RouterModule,
RouterTestingModule,
MockComponent(BannerText),
MockComponent(Navigation),
MockComponent(BulletinBoard)
]
});
fixture = TestBed.createComponent(Bulletins);
component = fixture.componentInstance;

View File

@ -26,6 +26,7 @@ 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';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [Bulletins],
@ -37,7 +38,8 @@ import { Navigation } from '../../../ui/common/navigation/navigation.component';
EffectsModule.forFeature(BulletinBoardEffects),
CounterListingModule,
BulletinBoard,
Navigation
Navigation,
BannerText
]
})
export class BulletinsModule {}

View File

@ -15,46 +15,48 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">NiFi Cluster</h3>
<error-banner></error-banner>
<div class="flex flex-col h-full gap-y-2">
@if (getTabLinks(); as tabs) {
<!-- Don't show the tab bar if there is only 1 tab to show -->
<div class="cluster-tabs" [class.hidden]="tabs.length === 1">
<nav mat-tab-nav-bar color="primary" [tabPanel]="tabPanel">
@for (tab of tabs; track tab) {
<a
mat-tab-link
[routerLink]="[tab.link]"
routerLinkActive
#rla="routerLinkActive"
[active]="rla.isActive">
{{ tab.label }}
</a>
}
</nav>
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">NiFi Cluster</h3>
<error-banner></error-banner>
<div class="flex flex-col h-full gap-y-2">
@if (getTabLinks(); as tabs) {
<!-- Don't show the tab bar if there is only 1 tab to show -->
<div class="cluster-tabs" [class.hidden]="tabs.length === 1">
<nav mat-tab-nav-bar color="primary" [tabPanel]="tabPanel">
@for (tab of tabs; track tab) {
<a
mat-tab-link
[routerLink]="[tab.link]"
routerLinkActive
#rla="routerLinkActive"
[active]="rla.isActive">
{{ tab.label }}
</a>
}
</nav>
</div>
}
<div class="mt-4 flex-1">
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</div>
}
<div class="pt-4 flex-1">
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</div>
<div class="flex justify-between align-middle">
<div class="text-sm flex items-center gap-x-2">
<button mat-icon-button color="primary" (click)="refresh()">
<i class="fa fa-refresh" [class.fa-spin]="listingStatus() === 'loading'"></i>
</button>
<div>Last updated:</div>
<div class="accent-color font-medium">{{ loadedTimestamp() }}</div>
<div class="flex justify-between align-middle">
<div class="text-sm flex items-center gap-x-2">
<button mat-icon-button color="primary" (click)="refresh()">
<i class="fa fa-refresh" [class.fa-spin]="listingStatus() === 'loading'"></i>
</button>
<div>Last updated:</div>
<div class="accent-color font-medium">{{ loadedTimestamp() }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</banner-text>

View File

@ -28,6 +28,7 @@ import { ClusterState } from '../state';
import { ErrorBanner } from '../../../ui/common/error-banner/error-banner.component';
import { MockComponent } from 'ng-mocks';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('Cluster', () => {
let component: Cluster;
@ -43,6 +44,7 @@ describe('Cluster', () => {
ClusterNodeListing,
MatTabsModule,
RouterTestingModule,
MockComponent(BannerText),
MockComponent(Navigation),
MockComponent(ErrorBanner)
],

View File

@ -27,6 +27,7 @@ import { ClusterRoutingModule } from './cluster-routing.module';
import { MatTabsModule } from '@angular/material/tabs';
import { MatIconButton } from '@angular/material/button';
import { ErrorBanner } from '../../../ui/common/error-banner/error-banner.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [Cluster],
@ -39,7 +40,8 @@ import { ErrorBanner } from '../../../ui/common/error-banner/error-banner.compon
EffectsModule.forFeature(ClusterListingEffects),
MatTabsModule,
MatIconButton,
ErrorBanner
ErrorBanner,
BannerText
]
})
export class ClusterModule {}

View File

@ -15,12 +15,14 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">NiFi Counters</h3>
<counter-listing class="flex-1"></counter-listing>
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">NiFi Counters</h3>
<counter-listing class="flex-1"></counter-listing>
</div>
</div>
</div>
</banner-text>

View File

@ -25,6 +25,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { countersFeatureKey } from '../state';
import { MockComponent } from 'ng-mocks';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('Counters', () => {
let component: Counters;
@ -33,7 +34,7 @@ describe('Counters', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Counters, CounterListing],
imports: [RouterModule, RouterTestingModule, MockComponent(Navigation)],
imports: [RouterModule, RouterTestingModule, MockComponent(BannerText), MockComponent(Navigation)],
providers: [
provideMockStore({
initialState: {

View File

@ -26,6 +26,7 @@ import { CounterListingEffects } from '../state/counter-listing/counter-listing.
import { CounterListingModule } from '../ui/counter-listing/counter-listing.module';
import { MatDialogModule } from '@angular/material/dialog';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [Counters],
@ -37,7 +38,8 @@ import { Navigation } from '../../../ui/common/navigation/navigation.component';
EffectsModule.forFeature(CounterListingEffects),
CounterListingModule,
MatDialogModule,
Navigation
Navigation,
BannerText
]
})
export class CountersModule {}

View File

@ -15,15 +15,17 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="p-2 flex flex-1 bg-white">
@if (frameSource) {
<iframe class="flex-1" [src]="frameSource"></iframe>
} @else {
<iframe class="flex-1" src="../nifi-docs/documentation"></iframe>
}
<banner-text>
<div class="flex flex-col h-full">
<header class="nifi-header">
<navigation></navigation>
</header>
<div class="p-2 flex flex-1 bg-white">
@if (frameSource) {
<iframe class="flex-1" [src]="frameSource"></iframe>
} @else {
<iframe class="flex-1" src="../nifi-docs/documentation"></iframe>
}
</div>
</div>
</div>
</banner-text>

View File

@ -21,10 +21,11 @@ import { Documentation } from './documentation.component';
import { DocumentationRoutingModule } from './documentation-routing.module';
import { MatDialogModule } from '@angular/material/dialog';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [Documentation],
exports: [Documentation],
imports: [CommonModule, MatDialogModule, DocumentationRoutingModule, Navigation]
imports: [CommonModule, MatDialogModule, DocumentationRoutingModule, Navigation, BannerText]
})
export class DocumentationModule {}

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<div class="error-background pt-24 pl-24 h-screen">
<div class="error-background pt-24 pl-24 h-full">
@if (errorDetail$ | async; as errorDetail) {
<page-content [title]="errorDetail.title">
<div class="text-base">{{ errorDetail.message }}</div>

View File

@ -15,12 +15,14 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">Flow Configuration History</h3>
<flow-configuration-history-listing class="flex-1"></flow-configuration-history-listing>
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">Flow Configuration History</h3>
<flow-configuration-history-listing class="flex-1"></flow-configuration-history-listing>
</div>
</div>
</div>
</banner-text>

View File

@ -23,6 +23,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { FlowConfigurationHistoryListing } from '../ui/flow-configuration-history-listing/flow-configuration-history-listing.component';
import { MockComponent } from 'ng-mocks';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('FlowConfigurationHistory', () => {
let component: FlowConfigurationHistory;
@ -34,6 +35,7 @@ describe('FlowConfigurationHistory', () => {
imports: [
RouterModule,
RouterTestingModule,
MockComponent(BannerText),
MockComponent(Navigation),
MockComponent(FlowConfigurationHistoryListing)
]

View File

@ -25,6 +25,7 @@ import { StoreModule } from '@ngrx/store';
import { flowConfigurationHistoryFeatureKey, reducers } from '../state';
import { EffectsModule } from '@ngrx/effects';
import { FlowConfigurationHistoryListingEffects } from '../state/flow-configuration-history-listing/flow-configuration-history-listing.effects';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
imports: [
@ -33,7 +34,8 @@ import { FlowConfigurationHistoryListingEffects } from '../state/flow-configurat
FlowConfigurationHistoryRoutingModule,
FlowConfigurationHistoryListing,
StoreModule.forFeature(flowConfigurationHistoryFeatureKey, reducers),
EffectsModule.forFeature(FlowConfigurationHistoryListingEffects)
EffectsModule.forFeature(FlowConfigurationHistoryListingEffects),
BannerText
],
declarations: [FlowConfigurationHistory],
exports: [FlowConfigurationHistory]

View File

@ -15,4 +15,6 @@
~ limitations under the License.
-->
<router-outlet></router-outlet>
<banner-text>
<router-outlet></router-outlet>
</banner-text>

View File

@ -21,6 +21,8 @@ import { FlowDesigner } from './flow-designer.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../state/flow/flow.reducer';
import { RouterTestingModule } from '@angular/router/testing';
import { MockComponent } from 'ng-mocks';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('FlowDesigner', () => {
let component: FlowDesigner;
@ -29,7 +31,7 @@ describe('FlowDesigner', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FlowDesigner],
imports: [RouterTestingModule],
imports: [RouterTestingModule, MockComponent(BannerText)],
providers: [
provideMockStore({
initialState

View File

@ -29,6 +29,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { ControllerServicesEffects } from '../state/controller-services/controller-services.effects';
import { ParameterEffects } from '../state/parameter/parameter.effects';
import { QueueEffects } from '../state/queue/queue.effects';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [FlowDesigner, VersionControlTip],
@ -45,7 +46,8 @@ import { QueueEffects } from '../state/queue/queue.effects';
QueueEffects
),
NgOptimizedImage,
MatDialogModule
MatDialogModule,
BannerText
]
})
export class FlowDesignerModule {}

View File

@ -15,12 +15,14 @@
~ limitations under the License.
-->
<div class="flex flex-col justify-between">
<div class="flex flex-col h-full">
<fd-header></fd-header>
<div class="flex-1">
<div id="canvas-container" class="canvas-background select-none" [cdkContextMenuTriggerFor]="contextMenu.menu">
<graph-controls></graph-controls>
</div>
<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>
</div>
<fd-footer></fd-footer>

View File

@ -16,11 +16,6 @@
*/
.canvas-background {
position: absolute;
top: 97px;
left: 0;
bottom: 33px;
right: 0;
background-size: 14px 14px;
z-index: 1;
overflow: hidden;

View File

@ -28,11 +28,6 @@
// Get hues from palette
.breadcrumb-container {
position: absolute;
bottom: 0;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.25);
height: 33px;
width: 100%;
background-color: var(--mat-app-background-color);
}
}

View File

@ -15,9 +15,11 @@
~ limitations under the License.
-->
<div class="graph-controls flex flex-col gap-y-0.5">
<navigation-control [shouldDockWhenCollapsed]="(operationCollapsed$ | async)!"></navigation-control>
<operation-control
[shouldDockWhenCollapsed]="(navigationCollapsed$ | async)!"
[breadcrumbEntity]="(breadcrumbEntity$ | async)!"></operation-control>
<div class="relative">
<div class="graph-controls flex flex-col gap-y-0.5">
<navigation-control [shouldDockWhenCollapsed]="(operationCollapsed$ | async)!"></navigation-control>
<operation-control
[shouldDockWhenCollapsed]="(navigationCollapsed$ | async)!"
[breadcrumbEntity]="(breadcrumbEntity$ | async)!"></operation-control>
</div>
</div>

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>

View File

@ -16,13 +16,13 @@
-->
@if (loading) {
<div class="splash h-screen p-20">
<div class="splash h-full p-20">
<div class="splash-img h-full flex items-center justify-center">
<mat-spinner color="warn"></mat-spinner>
</div>
</div>
} @else {
<div class="login-background pt-24 pl-24 h-screen">
<div class="login-background pt-24 pl-24 h-full">
@if (currentUserState$ | async; as userState) {
@if (userState.status === 'success') {
<page-content [title]="'Success'">

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<div class="logout-background pt-24 pl-24 h-screen">
<div class="logout-background pt-24 pl-24 h-full">
<page-content [title]="'Logout successful'">
<div class="text-sm">You have have successfully logged out. You may now close the window.</div>
</page-content>

View File

@ -18,9 +18,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Logout } from './logout.component';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/current-user/current-user.reducer';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MockComponent } from 'ng-mocks';
import { PageContent } from '../../../ui/common/page-content/page-content.component';
describe('Login', () => {
let component: Logout;
@ -29,8 +29,7 @@ describe('Login', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Logout],
imports: [MatProgressSpinner],
providers: [provideMockStore({ initialState })]
imports: [MatProgressSpinner, MockComponent(PageContent)]
});
fixture = TestBed.createComponent(Logout);
component = fixture.componentInstance;

View File

@ -15,12 +15,14 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">Parameter Contexts</h3>
<parameter-context-listing class="flex-1"></parameter-context-listing>
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">Parameter Contexts</h3>
<parameter-context-listing class="flex-1"></parameter-context-listing>
</div>
</div>
</div>
</banner-text>

View File

@ -23,6 +23,7 @@ import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MockComponent } from 'ng-mocks';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('ParameterContexts', () => {
let component: ParameterContexts;
@ -34,6 +35,7 @@ describe('ParameterContexts', () => {
imports: [
RouterModule,
RouterTestingModule,
MockComponent(BannerText),
MockComponent(Navigation),
MockComponent(ParameterContextListing)
]

View File

@ -25,6 +25,7 @@ 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';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [ParameterContexts],
@ -35,7 +36,8 @@ import { Navigation } from '../../../ui/common/navigation/navigation.component';
StoreModule.forFeature(parameterContextsFeatureKey, reducers),
EffectsModule.forFeature(ParameterContextListingEffects),
ParameterContextListingModule,
Navigation
Navigation,
BannerText
]
})
export class ParameterContextsModule {}

View File

@ -15,14 +15,16 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="px-5">
<h3 class="primary-color">Provenance</h3>
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="px-5">
<h3 class="primary-color">Provenance</h3>
</div>
<div class="pb-5 px-5 flex-1">
<router-outlet></router-outlet>
</div>
</div>
<div class="pb-5 px-5 flex-1">
<router-outlet></router-outlet>
</div>
</div>
</banner-text>

View File

@ -26,6 +26,7 @@ import { MockComponent } from 'ng-mocks';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { provenanceFeatureKey } from '../state';
import { provenanceEventListingFeatureKey } from '../state/provenance-event-listing';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('Provenance', () => {
let component: Provenance;
@ -34,7 +35,7 @@ describe('Provenance', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Provenance],
imports: [RouterModule, RouterTestingModule, MockComponent(Navigation)],
imports: [RouterModule, RouterTestingModule, MockComponent(BannerText), MockComponent(Navigation)],
providers: [
provideMockStore({
initialState: {

View File

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

View File

@ -15,11 +15,13 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1">
<router-outlet></router-outlet>
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1">
<router-outlet></router-outlet>
</div>
</div>
</div>
</banner-text>

View File

@ -22,6 +22,7 @@ import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MockComponent } from 'ng-mocks';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('Queue', () => {
let component: Queue;
@ -30,7 +31,7 @@ describe('Queue', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Queue],
imports: [RouterModule, RouterTestingModule, MockComponent(Navigation)]
imports: [RouterModule, RouterTestingModule, MockComponent(BannerText), MockComponent(Navigation)]
});
fixture = TestBed.createComponent(Queue);
component = fixture.componentInstance;

View File

@ -21,10 +21,11 @@ import { Queue } from './queue.component';
import { QueueRoutingModule } from './queue-routing.module';
import { MatDialogModule } from '@angular/material/dialog';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [Queue],
exports: [Queue],
imports: [CommonModule, MatDialogModule, QueueRoutingModule, Navigation]
imports: [CommonModule, MatDialogModule, QueueRoutingModule, Navigation, BannerText]
})
export class QueueModule {}

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<div class="error-background pt-24 pl-24 h-screen">
<div class="error-background pt-24 pl-24 h-full">
<page-content title="Route Not Found">
<div class="text-base">The URL entered is not recognized as a supported route.</div>
</page-content>

View File

@ -15,30 +15,32 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">NiFi Settings</h3>
<div class="settings-tabs">
<nav mat-tab-nav-bar color="primary" [tabPanel]="tabPanel">
@for (tab of tabLinks; track tab) {
<a
mat-tab-link
[routerLink]="[tab.link]"
routerLinkActive
#rla="routerLinkActive"
[active]="rla.isActive">
{{ tab.label }}
</a>
}
</nav>
</div>
<div class="pt-5 flex-1">
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">NiFi Settings</h3>
<div class="settings-tabs">
<nav mat-tab-nav-bar color="primary" [tabPanel]="tabPanel">
@for (tab of tabLinks; track tab) {
<a
mat-tab-link
[routerLink]="[tab.link]"
routerLinkActive
#rla="routerLinkActive"
[active]="rla.isActive">
{{ tab.label }}
</a>
}
</nav>
</div>
<div class="mt-5 flex-1">
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</div>
</div>
</div>
</div>
</banner-text>

View File

@ -27,6 +27,7 @@ import { MockComponent } from 'ng-mocks';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { settingsFeatureKey } from '../state';
import { generalFeatureKey } from '../state/general';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('Settings', () => {
let component: Settings;
@ -35,7 +36,13 @@ describe('Settings', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Settings],
imports: [MatTabsModule, RouterModule, RouterTestingModule, MockComponent(Navigation)],
imports: [
MatTabsModule,
RouterModule,
RouterTestingModule,
MockComponent(BannerText),
MockComponent(Navigation)
],
providers: [
provideMockStore({
initialState: {

View File

@ -36,6 +36,7 @@ import { RegistryClientsEffects } from '../state/registry-clients/registry-clien
import { FlowAnalysisRulesEffects } from '../state/flow-analysis-rules/flow-analysis-rules.effects';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { ParameterProvidersEffects } from '../state/parameter-providers/parameter-providers.effects';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [Settings],
@ -59,7 +60,8 @@ import { ParameterProvidersEffects } from '../state/parameter-providers/paramete
ParameterProvidersEffects
),
MatTabsModule,
Navigation
Navigation,
BannerText
]
})
export class SettingsModule {}

View File

@ -15,40 +15,42 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">
@if (selectedClusterNode$ | async; as selectedNode) {
@if (selectedNode.id !== 'All') {
{{ selectedNode.address }} Summary
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="primary-color">
@if (selectedClusterNode$ | async; as selectedNode) {
@if (selectedNode.id !== 'All') {
{{ selectedNode.address }} Summary
} @else {
NiFi Summary
}
} @else {
NiFi Summary
}
} @else {
NiFi Summary
}
</h3>
<div class="summary-tabs">
<nav mat-tab-nav-bar color="primary" [tabPanel]="tabPanel">
@for (tab of tabLinks; track tab) {
<a
mat-tab-link
[routerLink]="[tab.link]"
routerLinkActive
#rla="routerLinkActive"
[active]="rla.isActive">
{{ tab.label }}
</a>
}
</nav>
</div>
<div class="mt-5 flex-1">
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</h3>
<div class="summary-tabs">
<nav mat-tab-nav-bar color="primary" [tabPanel]="tabPanel">
@for (tab of tabLinks; track tab) {
<a
mat-tab-link
[routerLink]="[tab.link]"
routerLinkActive
#rla="routerLinkActive"
[active]="rla.isActive">
{{ tab.label }}
</a>
}
</nav>
</div>
<div class="mt-5 flex-1">
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</div>
</div>
</div>
</div>
</banner-text>

View File

@ -15,7 +15,9 @@
* limitations under the License.
*/
@use '@angular/material' as mat;
:host {
height: 100%;
}
.summary-tabs {
border-bottom-width: 1px;

View File

@ -26,6 +26,7 @@ import { MockComponent } from 'ng-mocks';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { summaryFeatureKey } from '../state';
import { summaryListingFeatureKey } from '../state/summary-listing';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('Summary', () => {
let component: Summary;
@ -34,7 +35,13 @@ describe('Summary', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Summary],
imports: [MatTabsModule, RouterModule, RouterTestingModule, MockComponent(Navigation)],
imports: [
MatTabsModule,
RouterModule,
RouterTestingModule,
MockComponent(BannerText),
MockComponent(Navigation)
],
providers: [
provideMockStore({
initialState: {

View File

@ -33,6 +33,7 @@ import { SummaryListingEffects } from '../state/summary-listing/summary-listing.
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { ComponentClusterStatusEffects } from '../state/component-cluster-status/component-cluster-status.effects';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [Summary],
@ -51,7 +52,8 @@ import { ComponentClusterStatusEffects } from '../state/component-cluster-status
StoreModule.forFeature(summaryFeatureKey, reducers),
EffectsModule.forFeature(SummaryListingEffects, ComponentClusterStatusEffects),
NgxSkeletonLoaderModule,
Navigation
Navigation,
BannerText
]
})
export class SummaryModule {}

View File

@ -18,7 +18,7 @@
<div class="summary-table-filter-container">
<div>
<form [formGroup]="filterForm" class="my-2">
<div class="flex mt-2 gap-1 items-baseline">
<div class="flex mt-2 gap-1 items-center">
<div>
<mat-form-field subscriptSizing="dynamic">
<mat-label>Filter</mat-label>

View File

@ -15,12 +15,14 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen justify-between">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="user-header primary-color">NiFi Users</h3>
<user-listing class="flex-1"></user-listing>
<banner-text>
<div class="flex flex-col h-full">
<header class="mb-5 nifi-header">
<navigation></navigation>
</header>
<div class="pb-5 px-5 flex-1 flex flex-col">
<h3 class="user-header primary-color">NiFi Users</h3>
<user-listing class="flex-1"></user-listing>
</div>
</div>
</div>
</banner-text>

View File

@ -25,6 +25,7 @@ import { initialState } from '../state/user-listing/user-listing.reducer';
import { MockComponent } from 'ng-mocks';
import { Navigation } from '../../../ui/common/navigation/navigation.component';
import { usersFeatureKey } from '../state';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
describe('Users', () => {
let component: Users;
@ -33,7 +34,7 @@ describe('Users', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Users, UserListing],
imports: [RouterModule, RouterTestingModule, MockComponent(Navigation)],
imports: [RouterModule, RouterTestingModule, MockComponent(BannerText), MockComponent(Navigation)],
providers: [
provideMockStore({
initialState: {

View File

@ -26,6 +26,7 @@ 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';
import { BannerText } from '../../../ui/common/banner-text/banner-text.component';
@NgModule({
declarations: [Users],
@ -37,7 +38,8 @@ import { Navigation } from '../../../ui/common/navigation/navigation.component';
EffectsModule.forFeature(UserListingEffects),
MatDialogModule,
UserListingModule,
Navigation
Navigation,
BannerText
]
})
export class UsersModule {}

View File

@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { BannerTextEntity } from '../state/banner-text';
@Injectable({ providedIn: 'root' })
export class BannerTextService {
private static readonly API: string = '../nifi-api';
constructor(private httpClient: HttpClient) {}
getBannerText(): Observable<BannerTextEntity> {
return this.httpClient.get<BannerTextEntity>(`${BannerTextService.API}/flow/banners`);
}
}

View File

@ -0,0 +1,26 @@
/*
* 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 { LoadBannerTextResponse } from './index';
export const loadBannerText = createAction('[Banner Text] Load Banner Text');
export const loadBannerTextSuccess = createAction(
'[Banner Text] Load Banner Text Success',
props<{ response: LoadBannerTextResponse }>()
);

View File

@ -0,0 +1,56 @@
/*
* 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 * as BannerTextActions from './banner-text.actions';
import { filter, from, map, switchMap } from 'rxjs';
import { BannerTextService } from '../../service/banner-text.service';
import { NiFiState } from '../index';
import { Store } from '@ngrx/store';
import { concatLatestFrom } from '@ngrx/operators';
import { selectBannerText } from './banner-text.selectors';
@Injectable()
export class BannerTextEffects {
constructor(
private actions$: Actions,
private bannerTextService: BannerTextService,
private store: Store<NiFiState>
) {}
loadBannerText$ = createEffect(() =>
this.actions$.pipe(
ofType(BannerTextActions.loadBannerText),
concatLatestFrom(() => this.store.select(selectBannerText)),
filter(([, bannerText]) => bannerText === null),
switchMap(() =>
from(
this.bannerTextService.getBannerText().pipe(
map((response) =>
BannerTextActions.loadBannerTextSuccess({
response: {
bannerText: response.banners
}
})
)
)
)
)
)
);
}

View File

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

View File

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

View File

@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const bannerTextFeatureKey = 'bannerText';
export interface LoadBannerTextResponse {
bannerText: BannerText;
}
export interface BannerTextEntity {
banners: BannerText;
}
export interface BannerText {
headerText: string;
footerText: string;
}
export interface BannerTextState {
bannerText: BannerText | null;
status: 'pending' | 'loading' | 'success';
}

View File

@ -45,6 +45,8 @@ import { propertyVerificationFeatureKey, PropertyVerificationState } from './pro
import { propertyVerificationReducer } from './property-verification/property-verification.reducer';
import { navigationFeatureKey, NavigationState } from './navigation';
import { navigationReducer } from './navigation/navigation.reducer';
import { bannerTextFeatureKey, BannerTextState } from './banner-text';
import { bannerTextReducer } from './banner-text/banner-text.reducer';
export interface NiFiState {
[DEFAULT_ROUTER_FEATURENAME]: RouterReducerState;
@ -52,6 +54,7 @@ export interface NiFiState {
[currentUserFeatureKey]: CurrentUserState;
[extensionTypesFeatureKey]: ExtensionTypesState;
[aboutFeatureKey]: AboutState;
[bannerTextFeatureKey]: BannerTextState;
[navigationFeatureKey]: NavigationState;
[flowConfigurationFeatureKey]: FlowConfigurationState;
[loginConfigurationFeatureKey]: LoginConfigurationState;
@ -70,6 +73,7 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
[currentUserFeatureKey]: currentUserReducer,
[extensionTypesFeatureKey]: extensionTypesReducer,
[aboutFeatureKey]: aboutReducer,
[bannerTextFeatureKey]: bannerTextReducer,
[navigationFeatureKey]: navigationReducer,
[flowConfigurationFeatureKey]: flowConfigurationReducer,
[loginConfigurationFeatureKey]: loginConfigurationReducer,

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<div class="flex flex-col h-screen">
<div class="flex flex-col h-full">
<header class="nifi-header">
<navigation></navigation>
</header>

View File

@ -0,0 +1,36 @@
<!--
~ 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="flex flex-col h-screen">
@if (bannerText(); as bannerText) {
@if (bannerText.headerText) {
<div class="flex justify-center">
{{ bannerText.headerText }}
</div>
}
}
<div class="flex-1">
<ng-content></ng-content>
</div>
@if (bannerText(); as bannerText) {
@if (bannerText.footerText) {
<div class="flex justify-center">
{{ bannerText.footerText }}
</div>
}
}
</div>

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,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 { BannerText } from './banner-text.component';
import { provideMockStore } from '@ngrx/store/testing';
import * as fromBannerText from '../../../state/banner-text/banner-text.reducer';
import { bannerTextFeatureKey } from '../../../state/banner-text';
describe('BannerText', () => {
let component: BannerText;
let fixture: ComponentFixture<BannerText>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [BannerText],
providers: [
provideMockStore({
initialState: {
[bannerTextFeatureKey]: fromBannerText.initialState
}
})
]
});
fixture = TestBed.createComponent(BannerText);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, OnInit } from '@angular/core';
import { loadBannerText } from '../../../state/banner-text/banner-text.actions';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../state';
import { selectBannerText } from '../../../state/banner-text/banner-text.selectors';
@Component({
selector: 'banner-text',
standalone: true,
templateUrl: './banner-text.component.html',
styleUrls: ['./banner-text.component.scss']
})
export class BannerText implements OnInit {
bannerText = this.store.selectSignal(selectBannerText);
constructor(private store: Store<NiFiState>) {}
ngOnInit(): void {
this.store.dispatch(loadBannerText());
}
}