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',
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',
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 { MatSnackBarModule } from '@angular/material/snack-bar';
import { PipesModule } from './pipes/pipes.module';
import { DocumentationEffects } from './state/documentation/documentation.effects';
@NgModule({
declarations: [AppComponent],
@ -71,7 +72,8 @@ import { PipesModule } from './pipes/pipes.module';
StatusHistoryEffects,
ControllerServiceStateEffects,
SystemDiagnosticsEffects,
ComponentStateEffects
ComponentStateEffects,
DocumentationEffects
),
StoreDevtoolsModule.instrument({
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';
import { promptEmptyQueueRequest, promptEmptyQueuesRequest } from '../state/queue/queue.actions';
import { getComponentStateAndOpenDialog } from '../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../state/documentation/documentation.actions';
@Injectable({ providedIn: 'root' })
export class CanvasContextMenu implements ContextMenuDefinitionProvider {
@ -717,13 +718,26 @@ export class CanvasContextMenu implements ContextMenuDefinitionProvider {
},
{
condition: (selection: any) => {
// TODO - hasUsage
return false;
return (
this.canvasUtils.canRead(selection) &&
selection.size() === 1 &&
this.canvasUtils.isProcessor(selection)
);
},
clazz: 'fa fa-book',
text: 'View usage',
action: () => {
// TODO - showUsage
text: 'View documentation',
action: (selection: any) => {
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"
[canModifyParent]="canModifyParent(serviceState.breadcrumb)"
(selectControllerService)="selectControllerService($event)"
(viewControllerServiceDocumentation)="viewControllerServiceDocumentation($event)"
(configureControllerService)="configureControllerService($event)"
(enableControllerService)="enableControllerService($event)"
(disableControllerService)="disableControllerService($event)"

View File

@ -46,6 +46,7 @@ import { selectFlowConfiguration } from '../../../../state/flow-configuration/fl
import { NiFiState } from '../../../../state';
import { loadFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.actions';
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
@Component({
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 {
this.store.dispatch(
navigateToEditService({

View File

@ -31,7 +31,10 @@
<td mat-cell *matCellDef="let item">
@if (canRead(item)) {
<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? -->
@if (hasComments(item)) {
<div>

View File

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

View File

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

View File

@ -41,6 +41,7 @@ import { selectCurrentUser } from '../../../../state/current-user/current-user.s
import { NiFiState } from '../../../../state';
import { FlowAnalysisRuleEntity, FlowAnalysisRulesState } from '../../state/flow-analysis-rules';
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
@Component({
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 {
this.store.dispatch(
enableFlowAnalysisRule({

View File

@ -40,6 +40,7 @@
[canModifyParent]="canModifyParent(currentUser)"
[flowConfiguration]="(flowConfiguration$ | async)!"
(selectControllerService)="selectControllerService($event)"
(viewControllerServiceDocumentation)="viewControllerServiceDocumentation($event)"
(configureControllerService)="configureControllerService($event)"
(enableControllerService)="enableControllerService($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 { CurrentUser } from '../../../../state/current-user';
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
@Component({
selector: 'management-controller-services',
@ -110,6 +111,19 @@ export class ManagementControllerServices implements OnInit, OnDestroy {
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 {
this.store.dispatch(
navigateToEditService({

View File

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

View File

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

View File

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

View File

@ -34,6 +34,7 @@ import { initialParameterProvidersState } from '../../state/parameter-providers/
import { switchMap, take } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isDefinedAndNotNull } from '../../../../state/shared';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
@Component({
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) {
this.store.dispatch(
ParameterProviderActions.promptParameterProviderDeletion({

View File

@ -31,7 +31,10 @@
<td mat-cell *matCellDef="let item">
@if (canRead(item)) {
<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? -->
@if (hasComments(item)) {
<div>

View File

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

View File

@ -39,6 +39,7 @@
(configureReportingTask)="configureReportingTask($event)"
(viewStateReportingTask)="viewStateReportingTask($event)"
(selectReportingTask)="selectReportingTask($event)"
(viewReportingTaskDocumentation)="viewReportingTaskDocumentation($event)"
(deleteReportingTask)="deleteReportingTask($event)"
(stopReportingTask)="stopReportingTask($event)"
(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 { selectFlowConfiguration } from '../../../../state/flow-configuration/flow-configuration.selectors';
import { getComponentStateAndOpenDialog } from '../../../../state/component-state/component-state.actions';
import { navigateToComponentDocumentation } from '../../../../state/documentation/documentation.actions';
@Component({
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 {
this.store.dispatch(
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 { errorFeatureKey, ErrorState } from './error';
import { errorReducer } from './error/error.reducer';
import { documentationFeatureKey, DocumentationState } from './documentation';
import { documentationReducer } from './documentation/documentation.reducer';
export interface NiFiState {
router: RouterReducerState;
@ -47,6 +49,7 @@ export interface NiFiState {
[controllerServiceStateFeatureKey]: ControllerServiceState;
[systemDiagnosticsFeatureKey]: SystemDiagnosticsState;
[componentStateFeatureKey]: ComponentStateState;
[documentationFeatureKey]: DocumentationState;
}
export const rootReducers: ActionReducerMap<NiFiState> = {
@ -59,5 +62,6 @@ export const rootReducers: ActionReducerMap<NiFiState> = {
[statusHistoryFeatureKey]: statusHistoryReducer,
[controllerServiceStateFeatureKey]: controllerServiceStateReducer,
[systemDiagnosticsFeatureKey]: systemDiagnosticsReducer,
[componentStateFeatureKey]: componentStateReducer
[componentStateFeatureKey]: componentStateReducer,
[documentationFeatureKey]: documentationReducer
};

View File

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

View File

@ -72,6 +72,8 @@ export class ControllerServiceTable {
@Output() selectControllerService: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@Output() viewControllerServiceDocumentation: EventEmitter<ControllerServiceEntity> =
new EventEmitter<ControllerServiceEntity>();
@Output() deleteControllerService: EventEmitter<ControllerServiceEntity> =
new 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 {
return !this.nifiCommon.isEmpty(entity.component.validationErrors);
}
@ -236,7 +243,7 @@ export class ControllerServiceTable {
this.isDisabled(entity) &&
this.canRead(entity) &&
this.canWrite(entity) &&
entity.component.multipleVersionsAvailable === true
entity.component.multipleVersionsAvailable
);
}

View File

@ -136,7 +136,7 @@
<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>
Help
</button>
@ -144,7 +144,10 @@
<i class="fa fa-fw fa-info-circle mr-2"></i>
About
</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 #theming="matMenu" xPosition="before">
<button mat-menu-item class="global-menu-item" (click)="toggleTheme(LIGHT_THEME)">