NIFI-12795: (#8416)

- Including embedded help documentation.
- Support linking to specific help documentation from the canvas and component listings.
This commit is contained in:
Matt Gilman 2024-02-16 10:01:04 -05:00 committed by GitHub
parent bd11031725
commit 6f6ddf8960
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 564 additions and 38 deletions

View File

@ -28,6 +28,12 @@ const routes: Routes = [
path: 'error', path: 'error',
loadChildren: () => import('./pages/error/feature/error.module').then((m) => m.ErrorModule) loadChildren: () => import('./pages/error/feature/error.module').then((m) => m.ErrorModule)
}, },
{
path: 'documentation',
canMatch: [authenticationGuard],
loadChildren: () =>
import('./pages/documentation/feature/documentation.module').then((m) => m.DocumentationModule)
},
{ {
path: 'settings', path: 'settings',
canMatch: [authenticationGuard], canMatch: [authenticationGuard],

View File

@ -45,6 +45,7 @@ import { ComponentStateEffects } from './state/component-state/component-state.e
import { ErrorEffects } from './state/error/error.effects'; import { ErrorEffects } from './state/error/error.effects';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { PipesModule } from './pipes/pipes.module'; import { PipesModule } from './pipes/pipes.module';
import { DocumentationEffects } from './state/documentation/documentation.effects';
@NgModule({ @NgModule({
declarations: [AppComponent], declarations: [AppComponent],
@ -71,7 +72,8 @@ import { PipesModule } from './pipes/pipes.module';
StatusHistoryEffects, StatusHistoryEffects,
ControllerServiceStateEffects, ControllerServiceStateEffects,
SystemDiagnosticsEffects, SystemDiagnosticsEffects,
ComponentStateEffects ComponentStateEffects,
DocumentationEffects
), ),
StoreDevtoolsModule.instrument({ StoreDevtoolsModule.instrument({
maxAge: 25, maxAge: 25,

View File

@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Documentation } from './documentation.component';
const routes: Routes = [
{
path: '',
component: Documentation
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DocumentationRoutingModule {}

View File

@ -0,0 +1,27 @@
<!--
~ 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="pb-5 flex flex-col h-screen justify-between">
<header class="nifi-header">
<navigation></navigation>
</header>
@if (frameSource) {
<iframe class="flex-1" [src]="frameSource"></iframe>
} @else {
<iframe class="flex-1" src="../nifi-docs/documentation"></iframe>
}
</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,52 @@
/*
* 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 { Documentation } from './documentation.component';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../../state/documentation/documentation.reducer';
describe('Documentation', () => {
let component: Documentation;
let fixture: ComponentFixture<Documentation>;
@Component({
selector: 'navigation',
standalone: true,
template: ''
})
class MockNavigation {}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Documentation],
imports: [RouterModule, RouterTestingModule, MockNavigation],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(Documentation);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,70 @@
/*
* 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, OnDestroy, SecurityContext } from '@angular/core';
import { NiFiState } from '../../../state';
import { Store } from '@ngrx/store';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { HttpParams } from '@angular/common/http';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { selectDocumentationParameters } from '../../../state/documentation/documentation.selectors';
import { DocumentationParameters } from '../../../state/documentation';
import { clearDocumentationParameters } from '../../../state/documentation/documentation.actions';
@Component({
selector: 'documentation',
templateUrl: './documentation.component.html',
styleUrls: ['./documentation.component.scss']
})
export class Documentation implements OnDestroy {
frameSource!: SafeResourceUrl | null;
constructor(
private store: Store<NiFiState>,
private domSanitizer: DomSanitizer
) {
this.store
.select(selectDocumentationParameters)
.pipe(takeUntilDestroyed())
.subscribe((params) => {
this.frameSource = this.getFrameSource(params);
});
}
private getFrameSource(params: DocumentationParameters | null): SafeResourceUrl | null {
let url = '../nifi-docs/documentation';
if (params) {
if (Object.keys(params).length > 0) {
const queryParams: string = new HttpParams({ fromObject: params }).toString();
url = `${url}?${queryParams}`;
}
const sanitizedUrl = this.domSanitizer.sanitize(SecurityContext.URL, url);
if (sanitizedUrl) {
return this.domSanitizer.bypassSecurityTrustResourceUrl(sanitizedUrl);
}
}
return null;
}
ngOnDestroy(): void {
this.store.dispatch(clearDocumentationParameters());
}
}

View File

@ -0,0 +1,30 @@
/*
* 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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
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';
@NgModule({
declarations: [Documentation],
exports: [Documentation],
imports: [CommonModule, MatDialogModule, DocumentationRoutingModule, Navigation]
})
export class DocumentationModule {}

View File

@ -57,6 +57,7 @@ import {
} from '../../../ui/common/context-menu/context-menu.component'; } from '../../../ui/common/context-menu/context-menu.component';
import { promptEmptyQueueRequest, promptEmptyQueuesRequest } from '../state/queue/queue.actions'; import { promptEmptyQueueRequest, promptEmptyQueuesRequest } from '../state/queue/queue.actions';
import { getComponentStateAndOpenDialog } from '../../../state/component-state/component-state.actions'; import { getComponentStateAndOpenDialog } from '../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../state/documentation/documentation.actions';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class CanvasContextMenu implements ContextMenuDefinitionProvider { export class CanvasContextMenu implements ContextMenuDefinitionProvider {
@ -717,13 +718,26 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
}, },
{ {
condition: (selection: any) => { condition: (selection: any) => {
// TODO - hasUsage return (
return false; this.canvasUtils.canRead(selection) &&
selection.size() === 1 &&
this.canvasUtils.isProcessor(selection)
);
}, },
clazz: 'fa fa-book', clazz: 'fa fa-book',
text: 'View usage', text: 'View documentation',
action: () => { action: (selection: any) => {
// TODO - showUsage const selectionData = selection.datum();
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: selectionData.component.type,
group: selectionData.component.bundle.group,
artifact: selectionData.component.bundle.artifact,
version: selectionData.component.bundle.version
}
})
);
} }
}, },
{ {

View File

@ -47,6 +47,7 @@
[flowConfiguration]="flowConfiguration" [flowConfiguration]="flowConfiguration"
[canModifyParent]="canModifyParent(serviceState.breadcrumb)" [canModifyParent]="canModifyParent(serviceState.breadcrumb)"
(selectControllerService)="selectControllerService($event)" (selectControllerService)="selectControllerService($event)"
(viewControllerServiceDocumentation)="viewControllerServiceDocumentation($event)"
(configureControllerService)="configureControllerService($event)" (configureControllerService)="configureControllerService($event)"
(enableControllerService)="enableControllerService($event)" (enableControllerService)="enableControllerService($event)"
(disableControllerService)="disableControllerService($event)" (disableControllerService)="disableControllerService($event)"

View File

@ -46,6 +46,7 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
import { NiFiState } from '../../../../state'; import { NiFiState } from '../../../../state';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions'; import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions'; import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
@Component({ @Component({
selector: 'controller-services', selector: 'controller-services',
@ -160,6 +161,19 @@ export class ControllerServices implements OnInit, OnDestroy {
}; };
} }
viewControllerServiceDocumentation(entity: ControllerServiceEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
}
})
);
}
configureControllerService(entity: ControllerServiceEntity): void { configureControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch( this.store.dispatch(
navigateToEditService({ navigateToEditService({

View File

@ -31,7 +31,10 @@
<td mat-cell *matCellDef="let item"> <td mat-cell *matCellDef="let item">
@if (canRead(item)) { @if (canRead(item)) {
<div class="flex items-center gap-x-3"> <div class="flex items-center gap-x-3">
<div class="pointer fa fa-book" title="Usage"></div> <div
class="pointer fa fa-book"
(click)="viewDocumentationClicked(item, $event)"
title="View Documentation"></div>
<!-- TODO - handle read only in configure component? --> <!-- TODO - handle read only in configure component? -->
@if (hasComments(item)) { @if (hasComments(item)) {
<div> <div>

View File

@ -51,10 +51,13 @@ export class FlowAnalysisRuleTable {
@Input() set flowAnalysisRules(flowAnalysisRuleEntities: FlowAnalysisRuleEntity[]) { @Input() set flowAnalysisRules(flowAnalysisRuleEntities: FlowAnalysisRuleEntity[]) {
this.dataSource.data = this.sortFlowAnalysisRules(flowAnalysisRuleEntities, this.sort); this.dataSource.data = this.sortFlowAnalysisRules(flowAnalysisRuleEntities, this.sort);
} }
@Input() selectedFlowAnalysisRuleId!: string; @Input() selectedFlowAnalysisRuleId!: string;
@Input() currentUser!: CurrentUser; @Input() currentUser!: CurrentUser;
@Output() selectFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); @Output() selectFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>();
@Output() viewFlowAnalysisRuleDocumentation: EventEmitter<FlowAnalysisRuleEntity> =
new EventEmitter<FlowAnalysisRuleEntity>();
@Output() deleteFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>(); @Output() deleteFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = new EventEmitter<FlowAnalysisRuleEntity>();
@Output() configureFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> = @Output() configureFlowAnalysisRule: EventEmitter<FlowAnalysisRuleEntity> =
new EventEmitter<FlowAnalysisRuleEntity>(); new EventEmitter<FlowAnalysisRuleEntity>();
@ -123,6 +126,11 @@ export class FlowAnalysisRuleTable {
return !!entity.operatePermissions?.canWrite; return !!entity.operatePermissions?.canWrite;
} }
viewDocumentationClicked(entity: FlowAnalysisRuleEntity, event: MouseEvent): void {
event.stopPropagation();
this.viewFlowAnalysisRuleDocumentation.next(entity);
}
hasComments(entity: FlowAnalysisRuleEntity): boolean { hasComments(entity: FlowAnalysisRuleEntity): boolean {
return !this.nifiCommon.isBlank(entity.component.comments); return !this.nifiCommon.isBlank(entity.component.comments);
} }
@ -236,7 +244,7 @@ export class FlowAnalysisRuleTable {
this.isDisabled(entity) && this.isDisabled(entity) &&
this.canRead(entity) && this.canRead(entity) &&
this.canWrite(entity) && this.canWrite(entity) &&
entity.component.multipleVersionsAvailable === true entity.component.multipleVersionsAvailable
); );
} }

View File

@ -37,6 +37,7 @@
[flowAnalysisRules]="flowAnalysisRuleState.flowAnalysisRules" [flowAnalysisRules]="flowAnalysisRuleState.flowAnalysisRules"
(configureFlowAnalysisRule)="configureFlowAnalysisRule($event)" (configureFlowAnalysisRule)="configureFlowAnalysisRule($event)"
(selectFlowAnalysisRule)="selectFlowAnalysisRule($event)" (selectFlowAnalysisRule)="selectFlowAnalysisRule($event)"
(viewFlowAnalysisRuleDocumentation)="viewFlowAnalysisRuleDocumentation($event)"
(enableFlowAnalysisRule)="enableFlowAnalysisRule($event)" (enableFlowAnalysisRule)="enableFlowAnalysisRule($event)"
(disableFlowAnalysisRule)="disableFlowAnalysisRule($event)" (disableFlowAnalysisRule)="disableFlowAnalysisRule($event)"
(viewStateFlowAnalysisRule)="viewStateFlowAnalysisRule($event)" (viewStateFlowAnalysisRule)="viewStateFlowAnalysisRule($event)"

View File

@ -41,6 +41,7 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import { NiFiState } from '../../../../state'; import { NiFiState } from '../../../../state';
import { FlowAnalysisRuleEntity, FlowAnalysisRulesState } from '../../state/flow-analysis-rules'; import { FlowAnalysisRuleEntity, FlowAnalysisRulesState } from '../../state/flow-analysis-rules';
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions'; import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
@Component({ @Component({
selector: 'flow-analysis-rules', selector: 'flow-analysis-rules',
@ -106,6 +107,19 @@ export class FlowAnalysisRules implements OnInit, OnDestroy {
); );
} }
viewFlowAnalysisRuleDocumentation(entity: FlowAnalysisRuleEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
}
})
);
}
enableFlowAnalysisRule(entity: FlowAnalysisRuleEntity): void { enableFlowAnalysisRule(entity: FlowAnalysisRuleEntity): void {
this.store.dispatch( this.store.dispatch(
enableFlowAnalysisRule({ enableFlowAnalysisRule({

View File

@ -40,6 +40,7 @@
[canModifyParent]="canModifyParent(currentUser)" [canModifyParent]="canModifyParent(currentUser)"
[flowConfiguration]="(flowConfiguration$ | async)!" [flowConfiguration]="(flowConfiguration$ | async)!"
(selectControllerService)="selectControllerService($event)" (selectControllerService)="selectControllerService($event)"
(viewControllerServiceDocumentation)="viewControllerServiceDocumentation($event)"
(configureControllerService)="configureControllerService($event)" (configureControllerService)="configureControllerService($event)"
(enableControllerService)="enableControllerService($event)" (enableControllerService)="enableControllerService($event)"
(disableControllerService)="disableControllerService($event)" (disableControllerService)="disableControllerService($event)"

View File

@ -45,6 +45,7 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions'; import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { CurrentUser } from '../../../../state/current-user'; import { CurrentUser } from '../../../../state/current-user';
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions'; import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
@Component({ @Component({
selector: 'management-controller-services', selector: 'management-controller-services',
@ -110,6 +111,19 @@ export class ManagementControllerServices implements OnInit, OnDestroy {
return true; return true;
} }
viewControllerServiceDocumentation(entity: ControllerServiceEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
}
})
);
}
configureControllerService(entity: ControllerServiceEntity): void { configureControllerService(entity: ControllerServiceEntity): void {
this.store.dispatch( this.store.dispatch(
navigateToEditService({ navigateToEditService({

View File

@ -31,25 +31,29 @@
<ng-container matColumnDef="moreDetails"> <ng-container matColumnDef="moreDetails">
<th mat-header-cell *matHeaderCellDef></th> <th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item"> <td mat-cell *matCellDef="let item">
<div class="flex items-center gap-x-3"> @if (canRead(item)) {
<!-- TODO: open details --> <div class="flex items-center gap-x-3">
<div class="pointer fa fa-info-circle" title="View details"></div> <!-- TODO: open details -->
<div class="pointer fa fa-info-circle" title="View details"></div>
<!-- TODO: open documentation --> <div
<div class="pointer fa fa-book" title="View Documentation"></div> class="pointer fa fa-book"
(click)="viewDocumentationClicked(item, $event)"
title="View Documentation"></div>
<!-- Validation Errors --> <!-- Validation Errors -->
@if (hasErrors(item)) { @if (hasErrors(item)) {
<div> <div>
<div <div
class="pointer fa fa-warning has-errors" class="pointer fa fa-warning has-errors"
nifiTooltip nifiTooltip
[delayClose]="false" [delayClose]="false"
[tooltipComponentType]="ValidationErrorsTip" [tooltipComponentType]="ValidationErrorsTip"
[tooltipInputData]="getValidationErrorsTipData(item)"></div> [tooltipInputData]="getValidationErrorsTipData(item)"></div>
</div> </div>
} }
</div> </div>
}
</td> </td>
</ng-container> </ng-container>

View File

@ -72,6 +72,8 @@ export class ParameterProvidersTable {
@Output() selectParameterProvider: EventEmitter<ParameterProviderEntity> = @Output() selectParameterProvider: EventEmitter<ParameterProviderEntity> =
new EventEmitter<ParameterProviderEntity>(); new EventEmitter<ParameterProviderEntity>();
@Output() viewParameterProviderDocumentation: EventEmitter<ParameterProviderEntity> =
new EventEmitter<ParameterProviderEntity>();
@Output() configureParameterProvider: EventEmitter<ParameterProviderEntity> = @Output() configureParameterProvider: EventEmitter<ParameterProviderEntity> =
new EventEmitter<ParameterProviderEntity>(); new EventEmitter<ParameterProviderEntity>();
@Output() deleteParameterProvider: EventEmitter<ParameterProviderEntity> = @Output() deleteParameterProvider: EventEmitter<ParameterProviderEntity> =
@ -129,6 +131,11 @@ export class ParameterProvidersTable {
return false; return false;
} }
viewDocumentationClicked(entity: ParameterProviderEntity, event: MouseEvent): void {
event.stopPropagation();
this.viewParameterProviderDocumentation.next(entity);
}
formatName(entity: ParameterProviderEntity): string { formatName(entity: ParameterProviderEntity): string {
return this.canRead(entity) ? entity.component.name : entity.id; return this.canRead(entity) ? entity.component.name : entity.id;
} }
@ -142,7 +149,7 @@ export class ParameterProvidersTable {
} }
hasErrors(entity: ParameterProviderEntity): boolean { hasErrors(entity: ParameterProviderEntity): boolean {
return this.canRead(entity) && !this.nifiCommon.isEmpty(entity.component.validationErrors); return !this.nifiCommon.isEmpty(entity.component.validationErrors);
} }
getValidationErrorsTipData(entity: ParameterProviderEntity): ValidationErrorsTipInput | null { getValidationErrorsTipData(entity: ParameterProviderEntity): ValidationErrorsTipInput | null {

View File

@ -42,6 +42,7 @@
(deleteParameterProvider)="deleteParameterProvider($event)" (deleteParameterProvider)="deleteParameterProvider($event)"
(configureParameterProvider)="openConfigureParameterProviderDialog($event)" (configureParameterProvider)="openConfigureParameterProviderDialog($event)"
(fetchParameterProvider)="fetchParameterProviderParameters($event)" (fetchParameterProvider)="fetchParameterProviderParameters($event)"
(viewParameterProviderDocumentation)="viewParameterProviderDocumentation($event)"
(selectParameterProvider)="selectParameterProvider($event)"></parameter-providers-table> (selectParameterProvider)="selectParameterProvider($event)"></parameter-providers-table>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">

View File

@ -34,6 +34,7 @@ import { initialParameterProvidersState } from '../../state/parameter-providers/
import { switchMap, take } from 'rxjs'; import { switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isDefinedAndNotNull } from '../../../../state/shared'; import { isDefinedAndNotNull } from '../../../../state/shared';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
@Component({ @Component({
selector: 'parameter-providers', selector: 'parameter-providers',
@ -130,6 +131,19 @@ export class ParameterProviders implements OnInit, OnDestroy {
); );
} }
viewParameterProviderDocumentation(parameterProvider: ParameterProviderEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: parameterProvider.component.type,
group: parameterProvider.component.bundle.group,
artifact: parameterProvider.component.bundle.artifact,
version: parameterProvider.component.bundle.version
}
})
);
}
deleteParameterProvider(parameterProvider: ParameterProviderEntity) { deleteParameterProvider(parameterProvider: ParameterProviderEntity) {
this.store.dispatch( this.store.dispatch(
ParameterProviderActions.promptParameterProviderDeletion({ ParameterProviderActions.promptParameterProviderDeletion({

View File

@ -31,7 +31,10 @@
<td mat-cell *matCellDef="let item"> <td mat-cell *matCellDef="let item">
@if (canRead(item)) { @if (canRead(item)) {
<div class="flex items-center gap-x-3"> <div class="flex items-center gap-x-3">
<div class="pointer fa fa-book" title="Usage"></div> <div
class="pointer fa fa-book"
(click)="viewDocumentationClicked(item, $event)"
title="View Documentation"></div>
<!-- TODO - handle read only in configure component? --> <!-- TODO - handle read only in configure component? -->
@if (hasComments(item)) { @if (hasComments(item)) {
<div> <div>

View File

@ -49,6 +49,8 @@ export class ReportingTaskTable {
@Input() currentUser!: CurrentUser; @Input() currentUser!: CurrentUser;
@Output() selectReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>(); @Output() selectReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() viewReportingTaskDocumentation: EventEmitter<ReportingTaskEntity> =
new EventEmitter<ReportingTaskEntity>();
@Output() deleteReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>(); @Output() deleteReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() startReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>(); @Output() startReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@Output() configureReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>(); @Output() configureReportingTask: EventEmitter<ReportingTaskEntity> = new EventEmitter<ReportingTaskEntity>();
@ -79,6 +81,11 @@ export class ReportingTaskTable {
return !!entity.operatePermissions?.canWrite; return !!entity.operatePermissions?.canWrite;
} }
viewDocumentationClicked(entity: ReportingTaskEntity, event: MouseEvent): void {
event.stopPropagation();
this.viewReportingTaskDocumentation.next(entity);
}
hasComments(entity: ReportingTaskEntity): boolean { hasComments(entity: ReportingTaskEntity): boolean {
return !this.nifiCommon.isBlank(entity.component.comments); return !this.nifiCommon.isBlank(entity.component.comments);
} }

View File

@ -39,6 +39,7 @@
(configureReportingTask)="configureReportingTask($event)" (configureReportingTask)="configureReportingTask($event)"
(viewStateReportingTask)="viewStateReportingTask($event)" (viewStateReportingTask)="viewStateReportingTask($event)"
(selectReportingTask)="selectReportingTask($event)" (selectReportingTask)="selectReportingTask($event)"
(viewReportingTaskDocumentation)="viewReportingTaskDocumentation($event)"
(deleteReportingTask)="deleteReportingTask($event)" (deleteReportingTask)="deleteReportingTask($event)"
(stopReportingTask)="stopReportingTask($event)" (stopReportingTask)="stopReportingTask($event)"
(startReportingTask)="startReportingTask($event)"></reporting-task-table> (startReportingTask)="startReportingTask($event)"></reporting-task-table>

View File

@ -43,6 +43,7 @@ import { NiFiState } from '../../../../state';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions'; import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors'; import { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions'; import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
@Component({ @Component({
selector: 'reporting-tasks', selector: 'reporting-tasks',
@ -110,6 +111,19 @@ export class ReportingTasks implements OnInit, OnDestroy {
); );
} }
viewReportingTaskDocumentation(entity: ReportingTaskEntity): void {
this.store.dispatch(
navigateToComponentDocumentation({
params: {
select: entity.component.type,
group: entity.component.bundle.group,
artifact: entity.component.bundle.artifact,
version: entity.component.bundle.version
}
})
);
}
deleteReportingTask(entity: ReportingTaskEntity): void { deleteReportingTask(entity: ReportingTaskEntity): void {
this.store.dispatch( this.store.dispatch(
promptReportingTaskDeletion({ promptReportingTaskDeletion({

View File

@ -0,0 +1,28 @@
/*
* 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 { DocumentationParameters } from './index';
export const navigateToComponentDocumentation = createAction(
'[Documentation] Navigate To Component Documentation',
props<{
params: DocumentationParameters;
}>()
);
export const clearDocumentationParameters = createAction('[Documentation] Clear Documentation Parameters');

View File

@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as DocumentationActions from './documentation.actions';
import { tap } from 'rxjs';
import { Router } from '@angular/router';
@Injectable()
export class DocumentationEffects {
constructor(
private actions$: Actions,
private router: Router
) {}
navigateToComponentDocumentation$ = createEffect(
() =>
this.actions$.pipe(
ofType(DocumentationActions.navigateToComponentDocumentation),
tap(() => {
this.router.navigate(['/documentation']);
})
),
{ dispatch: false }
);
}

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.
*/
import { createReducer, on } from '@ngrx/store';
import { DocumentationState } from './index';
import { clearDocumentationParameters, navigateToComponentDocumentation } from './documentation.actions';
export const initialState: DocumentationState = {
documentationParameters: null
};
export const documentationReducer = createReducer(
initialState,
on(navigateToComponentDocumentation, (state, { params }) => ({
...state,
documentationParameters: params
})),
on(clearDocumentationParameters, (state) => ({
...state,
documentationParameters: null
}))
);

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 { createFeatureSelector, createSelector } from '@ngrx/store';
import { documentationFeatureKey, DocumentationState } from './index';
export const selectDocumentationState = createFeatureSelector<DocumentationState>(documentationFeatureKey);
export const selectDocumentationParameters = createSelector(
selectDocumentationState,
(state: DocumentationState) => state.documentationParameters
);

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.
*/
export const documentationFeatureKey = 'documentation';
export interface DocumentationParameters {
[key: string]: string;
}
export interface DocumentationState {
documentationParameters: DocumentationParameters | null;
}

View File

@ -35,6 +35,8 @@ import { componentStateFeatureKey, ComponentStateState } from './component-state
import { componentStateReducer } from './component-state/component-state.reducer'; import { componentStateReducer } from './component-state/component-state.reducer';
import { errorFeatureKey, ErrorState } from './error'; import { errorFeatureKey, ErrorState } from './error';
import { errorReducer } from './error/error.reducer'; import { errorReducer } from './error/error.reducer';
import { documentationFeatureKey, DocumentationState } from './documentation';
import { documentationReducer } from './documentation/documentation.reducer';
export interface NiFiState { export interface NiFiState {
router: RouterReducerState; router: RouterReducerState;
@ -47,6 +49,7 @@ export interface NiFiState {
[controllerServiceStateFeatureKey]: ControllerServiceState; [controllerServiceStateFeatureKey]: ControllerServiceState;
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState; [systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
[componentStateFeatureKey]: ComponentStateState; [componentStateFeatureKey]: ComponentStateState;
[documentationFeatureKey]: DocumentationState;
} }
export const rootReducers: ActionReducerMap<NiFiState> = { export const rootReducers: ActionReducerMap<NiFiState> = {
@ -59,5 +62,6 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
[statusHistoryFeatureKey]: statusHistoryReducer, [statusHistoryFeatureKey]: statusHistoryReducer,
[controllerServiceStateFeatureKey]: controllerServiceStateReducer, [controllerServiceStateFeatureKey]: controllerServiceStateReducer,
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer, [systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
[componentStateFeatureKey]: componentStateReducer [componentStateFeatureKey]: componentStateReducer,
[documentationFeatureKey]: documentationReducer
}; };

View File

@ -31,13 +31,11 @@
<td mat-cell *matCellDef="let item"> <td mat-cell *matCellDef="let item">
@if (canRead(item)) { @if (canRead(item)) {
<div class="flex items-center gap-x-3"> <div class="flex items-center gap-x-3">
<div class="pointer fa fa-book" title="Usage"></div> <div
<!-- class="pointer fa fa-book"
nifiTooltip dynamically inserts the tooltip component (click)="viewDocumentationClicked(item, $event)"
into the dom. the dom placement caused shifting of icons title="View Documentation"></div>
because of the gap between items. simple solution is to
just wrap the target.
-->
@if (hasComments(item)) { @if (hasComments(item)) {
<div> <div>
<div <div

View File

@ -72,6 +72,8 @@ export class ControllerServiceTable {
@Output() selectControllerService: EventEmitter<ControllerServiceEntity> = @Output() selectControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>(); new EventEmitter<ControllerServiceEntity>();
@Output() viewControllerServiceDocumentation: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@Output() deleteControllerService: EventEmitter<ControllerServiceEntity> = @Output() deleteControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>(); new EventEmitter<ControllerServiceEntity>();
@Output() configureControllerService: EventEmitter<ControllerServiceEntity> = @Output() configureControllerService: EventEmitter<ControllerServiceEntity> =
@ -117,6 +119,11 @@ export class ControllerServiceTable {
}; };
} }
viewDocumentationClicked(entity: ControllerServiceEntity, event: MouseEvent): void {
event.stopPropagation();
this.viewControllerServiceDocumentation.next(entity);
}
hasErrors(entity: ControllerServiceEntity): boolean { hasErrors(entity: ControllerServiceEntity): boolean {
return !this.nifiCommon.isEmpty(entity.component.validationErrors); return !this.nifiCommon.isEmpty(entity.component.validationErrors);
} }
@ -236,7 +243,7 @@ export class ControllerServiceTable {
this.isDisabled(entity) && this.isDisabled(entity) &&
this.canRead(entity) && this.canRead(entity) &&
this.canWrite(entity) && this.canWrite(entity) &&
entity.component.multipleVersionsAvailable === true entity.component.multipleVersionsAvailable
); );
} }

View File

@ -136,7 +136,7 @@
<mat-divider></mat-divider> <mat-divider></mat-divider>
} }
} }
<button mat-menu-item class="global-menu-item"> <button mat-menu-item class="global-menu-item" [routerLink]="['/documentation']">
<i class="fa fa-fw fa-question-circle mr-2"></i> <i class="fa fa-fw fa-question-circle mr-2"></i>
Help Help
</button> </button>
@ -144,7 +144,10 @@
<i class="fa fa-fw fa-info-circle mr-2"></i> <i class="fa fa-fw fa-info-circle mr-2"></i>
About About
</button> </button>
<button mat-menu-item [matMenuTriggerFor]="theming">Appearance</button> <button mat-menu-item [matMenuTriggerFor]="theming">
<i class="fa fa-fw mr-2"></i>
Appearance
</button>
</mat-menu> </mat-menu>
<mat-menu #theming="matMenu" xPosition="before"> <mat-menu #theming="matMenu" xPosition="before">
<button mat-menu-item class="global-menu-item" (click)="toggleTheme(LIGHT_THEME)"> <button mat-menu-item class="global-menu-item" (click)="toggleTheme(LIGHT_THEME)">