This is an automated email from the ASF dual-hosted git repository.
mcgilman pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 0271e926eb [NIFI-13318] processor stop and configure UX (#9548)
0271e926eb is described below
commit 0271e926eba51563e52ceceff393c382b37c4582
Author: Scott Aslan <[email protected]>
AuthorDate: Tue Dec 10 16:54:30 2024 -0500
[NIFI-13318] processor stop and configure UX (#9548)
* [NIFI-13318] processor stop and configure UX
* handle invalid run status
* display validation errors
* invalid icon and tooltip alternative placement
* remove validation errors tooltip, move invalid icon into status button,
update edit processor entity and readonly updates
* restore MAT_DIALOG_DATA and only enable/disable form controls via api
* clean up
* only allow updates by current client and poll until stopped and no active
threads
* update menu options to display when available
* display processor bulletins
* align dialog header text and run status button
* update filter for incoming updated entities and submit appropriate
revision on run status changes
* disable button when stopping
* code clean up
* update method name
* update error message
* update types
* add types and cleanup
* move run status action button
* review feedback
* update run status action button to consider when user cannot operate
processor
* update to also handle disabled run status when user does not have operate
* clean up pollingProcessor
* disable button when stopping
* prettier
* prettier
* readd thread count
* poll when necessary
This closes #9548
---
.../apache/nifi/web/StandardNiFiServiceFacade.java | 2 +-
nifi-frontend/src/main/frontend/.gitignore | 1 +
.../pages/flow-designer/state/flow/flow.actions.ts | 15 ++
.../pages/flow-designer/state/flow/flow.effects.ts | 174 +++++++++++++++++
.../pages/flow-designer/state/flow/flow.reducer.ts | 14 +-
.../flow-designer/state/flow/flow.selectors.ts | 2 +
.../app/pages/flow-designer/state/flow/index.ts | 5 +
.../flow-status/_flow-status.component-theme.scss | 5 -
.../_edit-processor.component-theme.scss} | 43 +----
.../edit-processor/edit-processor.component.html | 149 +++++++++++---
.../edit-processor/edit-processor.component.ts | 215 +++++++++++++++++++--
.../apps/nifi/src/app/service/client.service.ts | 14 +-
.../src/main/frontend/apps/nifi/src/styles.scss | 3 +
.../libs/shared/src/assets/styles/_app.scss | 1 +
.../frontend/libs/shared/src/services/index.ts | 1 +
.../{index.ts => session-storage.service.spec.ts} | 20 +-
.../shared/src/services/session-storage.service.ts | 110 +++++++++++
17 files changed, 685 insertions(+), 89 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 824c28d2e3..ad5f200f5d 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -524,7 +524,7 @@ public class StandardNiFiServiceFacade implements
NiFiServiceFacade {
return;
}
- throw new InvalidRevisionException(revision + " is not the most
up-to-date revision. This component appears to have been modified");
+ throw new InvalidRevisionException(revision + " is not the most
up-to-date revision. This component appears to have been modified. Retrieve the
most up-to-date revision and try again.");
}
@Override
diff --git a/nifi-frontend/src/main/frontend/.gitignore
b/nifi-frontend/src/main/frontend/.gitignore
index 8224631ef2..581343e403 100644
--- a/nifi-frontend/src/main/frontend/.gitignore
+++ b/nifi-frontend/src/main/frontend/.gitignore
@@ -36,6 +36,7 @@ yarn-error.log
/libpeerconnection.log
testem.log
/typings
+/.tool-versions
# System files
.DS_Store
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
index a5882f5396..4850df9ab5 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.actions.ts
@@ -87,6 +87,7 @@ import {
StartComponentRequest,
StartComponentResponse,
StartComponentsRequest,
+ StartPollingProcessorUntilStoppedRequest,
StartProcessGroupRequest,
StartProcessGroupResponse,
StopComponentRequest,
@@ -776,6 +777,20 @@ export const pollChangeVersionSuccess = createAction(
export const stopPollingChangeVersion = createAction(`${CANVAS_PREFIX} Stop
Polling Change Version`);
+export const startPollingProcessorUntilStopped = createAction(
+ `${CANVAS_PREFIX} Start Polling Processor Until Stopped`,
+ props<{ request: StartPollingProcessorUntilStoppedRequest }>()
+);
+
+export const pollProcessorUntilStopped = createAction(`${CANVAS_PREFIX} Poll
Processor Until Stopped`);
+
+export const pollProcessorUntilStoppedSuccess = createAction(
+ `${CANVAS_PREFIX} Poll Processor Until Stopped Success`,
+ props<{ response: LoadProcessorSuccess }>()
+);
+
+export const stopPollingProcessor = createAction(`${CANVAS_PREFIX} Stop
Polling Processor`);
+
export const openSaveVersionDialog = createAction(
`${CANVAS_PREFIX} Open Save Flow Version Dialog`,
props<{ request: SaveVersionDialogRequest }>()
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
index 576f57114b..7819a895be 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.effects.ts
@@ -45,6 +45,8 @@ import {
CreateConnectionDialogRequest,
CreateProcessGroupDialogRequest,
DeleteComponentResponse,
+ DisableComponentRequest,
+ EnableComponentRequest,
GroupComponentsDialogRequest,
ImportFromRegistryDialogRequest,
LoadProcessGroupResponse,
@@ -56,6 +58,8 @@ import {
SaveVersionRequest,
SelectedComponent,
Snippet,
+ StartComponentRequest,
+ StopComponentRequest,
StopVersionControlRequest,
StopVersionControlResponse,
UpdateComponentFailure,
@@ -80,6 +84,7 @@ import {
selectParentProcessGroupId,
selectProcessGroup,
selectProcessor,
+ selectPollingProcessor,
selectRefreshRpgDetails,
selectRemoteProcessGroup,
selectSaving,
@@ -160,6 +165,13 @@ import { selectDocumentVisibilityState } from
'../../../../state/document-visibi
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DocumentVisibility } from '../../../../state/document-visibility';
import { ErrorContextKey } from '../../../../state/error';
+import {
+ disableComponent,
+ enableComponent,
+ startComponent,
+ startPollingProcessorUntilStopped,
+ stopComponent
+} from './flow.actions';
import { CopyPasteService } from '../../service/copy-paste.service';
import { selectCopiedContent } from '../../../../state/copy/copy.selectors';
import { CopyRequestContext, CopyResponseContext } from
'../../../../state/copy';
@@ -1428,6 +1440,7 @@ export class FlowEffects {
}),
tap(([request, parameterContext, processGroupId]) => {
const processorId: string = request.entity.id;
+ let runStatusChanged: boolean = false;
const editDialogReference =
this.dialog.open(EditProcessor, {
...XL_DIALOG,
@@ -1555,6 +1568,116 @@ export class FlowEffects {
})
);
});
+ const startPollingIfNecessary = (processorEntity: any):
boolean => {
+ if (
+
(processorEntity.status.aggregateSnapshot.runStatus === 'Stopped' &&
+
processorEntity.status.aggregateSnapshot.activeThreadCount > 0) ||
+ processorEntity.status.aggregateSnapshot.runStatus
=== 'Validating'
+ ) {
+ this.store.dispatch(
+ startPollingProcessorUntilStopped({
+ request: {
+ id: processorEntity.id
+ }
+ })
+ );
+ return true;
+ }
+
+ return false;
+ };
+
+ const pollingStarted =
startPollingIfNecessary(request.entity);
+
+ this.store
+ .select(selectProcessor(processorId))
+ .pipe(
+ takeUntil(editDialogReference.afterClosed()),
+ isDefinedAndNotNull(),
+ filter((processorEntity) => {
+ return (
+ (runStatusChanged || pollingStarted) &&
+ processorEntity.revision.clientId ===
this.client.getClientId()
+ );
+ }),
+ concatLatestFrom(() =>
this.store.select(selectPollingProcessor))
+ )
+ .subscribe(([processorEntity, pollingProcessor]) => {
+
editDialogReference.componentInstance.processorUpdates = processorEntity;
+
+ // if we're already polling we do not want to
start polling again
+ if (!pollingProcessor) {
+ startPollingIfNecessary(processorEntity);
+ }
+ });
+
+ editDialogReference.componentInstance.stopComponentRequest
+ .pipe(takeUntil(editDialogReference.afterClosed()))
+ .subscribe((stopComponentRequest:
StopComponentRequest) => {
+ runStatusChanged = true;
+ this.store.dispatch(
+ stopComponent({
+ request: {
+ id: stopComponentRequest.id,
+ uri: stopComponentRequest.uri,
+ type: ComponentType.Processor,
+ revision:
stopComponentRequest.revision,
+ errorStrategy: 'snackbar'
+ }
+ })
+ );
+ });
+
+
editDialogReference.componentInstance.disableComponentRequest
+ .pipe(takeUntil(editDialogReference.afterClosed()))
+ .subscribe((disableComponentsRequest:
DisableComponentRequest) => {
+ runStatusChanged = true;
+ this.store.dispatch(
+ disableComponent({
+ request: {
+ id: disableComponentsRequest.id,
+ uri: disableComponentsRequest.uri,
+ type: ComponentType.Processor,
+ revision:
disableComponentsRequest.revision,
+ errorStrategy: 'snackbar'
+ }
+ })
+ );
+ });
+
+
editDialogReference.componentInstance.enableComponentRequest
+ .pipe(takeUntil(editDialogReference.afterClosed()))
+ .subscribe((enableComponentsRequest:
EnableComponentRequest) => {
+ runStatusChanged = true;
+ this.store.dispatch(
+ enableComponent({
+ request: {
+ id: enableComponentsRequest.id,
+ uri: enableComponentsRequest.uri,
+ type: ComponentType.Processor,
+ revision:
enableComponentsRequest.revision,
+ errorStrategy: 'snackbar'
+ }
+ })
+ );
+ });
+
+ editDialogReference.componentInstance.startComponentRequest
+ .pipe(takeUntil(editDialogReference.afterClosed()))
+ .subscribe((startComponentRequest:
StartComponentRequest) => {
+ runStatusChanged = true;
+ this.store.dispatch(
+ startComponent({
+ request: {
+ id: startComponentRequest.id,
+ uri: startComponentRequest.uri,
+ type: ComponentType.Processor,
+ revision:
startComponentRequest.revision,
+ errorStrategy: 'snackbar'
+ }
+ })
+ );
+ });
editDialogReference.afterClosed().subscribe((response) => {
this.store.dispatch(resetPropertyVerificationState());
@@ -1578,6 +1701,57 @@ export class FlowEffects {
{ dispatch: false }
);
+ startPollingProcessorUntilStopped = createEffect(() =>
+ this.actions$.pipe(
+ ofType(FlowActions.startPollingProcessorUntilStopped),
+ switchMap(() =>
+ interval(2000, asyncScheduler).pipe(
+
takeUntil(this.actions$.pipe(ofType(FlowActions.stopPollingProcessor)))
+ )
+ ),
+ switchMap(() => of(FlowActions.pollProcessorUntilStopped()))
+ )
+ );
+
+ pollProcessorUntilStopped$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(FlowActions.pollProcessorUntilStopped),
+ concatLatestFrom(() =>
[this.store.select(selectPollingProcessor).pipe(isDefinedAndNotNull())]),
+ switchMap(([, pollingProcessor]) => {
+ return from(
+ this.flowService.getProcessor(pollingProcessor.id).pipe(
+ map((response) =>
+ FlowActions.pollProcessorUntilStoppedSuccess({
+ response: {
+ id: pollingProcessor.id,
+ processor: response
+ }
+ })
+ ),
+ catchError((errorResponse: HttpErrorResponse) => {
+
this.store.dispatch(FlowActions.stopPollingProcessor());
+ return
of(this.snackBarOrFullScreenError(errorResponse));
+ })
+ )
+ );
+ })
+ )
+ );
+
+ pollProcessorUntilStoppedSuccess$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(FlowActions.pollProcessorUntilStoppedSuccess),
+ map((action) => action.response),
+ filter((response) => {
+ return (
+ response.processor.status.runStatus === 'Stopped' &&
+
response.processor.status.aggregateSnapshot.activeThreadCount === 0
+ );
+ }),
+ switchMap(() => of(FlowActions.stopPollingProcessor()))
+ )
+ );
+
openEditConnectionDialog$ = createEffect(
() =>
this.actions$.pipe(
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
index cd3ec9f31b..4cff6a7b2d 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.reducer.ts
@@ -50,6 +50,7 @@ import {
navigateWithoutTransform,
pasteSuccess,
pollChangeVersionSuccess,
+ pollProcessorUntilStoppedSuccess,
pollRevertChangesSuccess,
requestRefreshRemoteProcessGroup,
resetFlowState,
@@ -68,10 +69,12 @@ import {
setTransitionRequired,
startComponent,
startComponentSuccess,
+ startPollingProcessorUntilStopped,
startProcessGroupSuccess,
startRemoteProcessGroupPolling,
stopComponent,
stopComponentSuccess,
+ stopPollingProcessor,
stopProcessGroupSuccess,
stopRemoteProcessGroupPolling,
stopVersionControl,
@@ -92,6 +95,7 @@ import { produce } from 'immer';
export const initialState: FlowState = {
id: 'root',
changeVersionRequest: null,
+ pollingProcessor: null,
flow: {
revision: {
version: 0
@@ -297,7 +301,7 @@ export const flowReducer = createReducer(
}
});
}),
- on(loadProcessorSuccess, (state, { response }) => {
+ on(loadProcessorSuccess, pollProcessorUntilStoppedSuccess, (state, {
response }) => {
return produce(state, (draftState) => {
const proposedProcessor = response.processor;
const componentIndex: number =
draftState.flow.processGroupFlow.flow.processors.findIndex(
@@ -373,6 +377,14 @@ export const flowReducer = createReducer(
saving: false,
versionSaving: false
})),
+ on(startPollingProcessorUntilStopped, (state, { request }) => ({
+ ...state,
+ pollingProcessor: request
+ })),
+ on(stopPollingProcessor, (state) => ({
+ ...state,
+ pollingProcessor: null
+ })),
on(
createProcessor,
createProcessGroup,
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
index 101dcb8c77..31e04c68e7 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/flow.selectors.ts
@@ -30,6 +30,8 @@ export const selectChangeVersionRequest = createSelector(
(state: FlowState) => state.changeVersionRequest
);
+export const selectPollingProcessor = createSelector(selectFlowState, (state:
FlowState) => state.pollingProcessor);
+
export const selectSaving = createSelector(selectFlowState, (state: FlowState)
=> state.saving);
export const selectVersionSaving = createSelector(selectFlowState, (state:
FlowState) => state.versionSaving);
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
index eea1eea233..5160c70c8f 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/state/flow/index.ts
@@ -659,6 +659,7 @@ export interface FlowState {
flowAnalysisOpen: boolean;
versionSaving: boolean;
changeVersionRequest: FlowUpdateRequestEntity | null;
+ pollingProcessor: StartPollingProcessorUntilStoppedRequest | null;
status: 'pending' | 'loading' | 'success' | 'complete';
}
@@ -792,6 +793,10 @@ export interface StopComponentRequest {
errorStrategy: 'snackbar' | 'banner';
}
+export interface StartPollingProcessorUntilStoppedRequest {
+ id: string;
+}
+
export interface StopProcessGroupRequest {
id: string;
type: ComponentType;
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
index d753fa35ad..08e581d217 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
@@ -40,11 +40,6 @@
neutral,
map.get(map.get($config, neutral), lighter)
);
- $material-theme-neutral-palette-default: mat.get-theme-color(
- $material-theme,
- neutral,
- map.get(map.get($config, neutral), default)
- );
$material-theme-primary-palette-default: mat.get-theme-color(
$material-theme,
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/_edit-processor.component-theme.scss
similarity index 68%
copy from
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
copy to
nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/_edit-processor.component-theme.scss
index d753fa35ad..f7cfc4563f 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/header/flow-status/_flow-status.component-theme.scss
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/_edit-processor.component-theme.scss
@@ -30,21 +30,6 @@
error,
map.get(map.get($config, error), default)
);
- $material-theme-neutral-palette-darker: mat.get-theme-color(
- $material-theme,
- neutral,
- map.get(map.get($config, neutral), darker)
- );
- $material-theme-neutral-palette-lighter: mat.get-theme-color(
- $material-theme,
- neutral,
- map.get(map.get($config, neutral), lighter)
- );
- $material-theme-neutral-palette-default: mat.get-theme-color(
- $material-theme,
- neutral,
- map.get(map.get($config, neutral), default)
- );
$material-theme-primary-palette-default: mat.get-theme-color(
$material-theme,
@@ -58,14 +43,8 @@
$success: map.get(map.get($config, success), default);
$caution: map.get(map.get($config, caution), default);
- .flow-status {
- background-color: if(
- $is-material-dark,
- $material-theme-neutral-palette-darker,
- $material-theme-neutral-palette-lighter
- );
-
- .controller-bulletins {
+ #edit-processor-header {
+ .bulletins {
background-color: unset;
.fa {
@@ -73,7 +52,7 @@
}
}
- .controller-bulletins.has-bulletins {
+ .bulletins.has-bulletins {
.fa {
color: $primary-contrast;
}
@@ -98,21 +77,5 @@
background-color: $success;
}
}
-
- .flow-analysis-notifications.warn {
- background-color: $material-theme-secondary-palette-default;
-
- .fa {
- color: $primary-contrast;
- }
- }
-
- .flow-analysis-notifications.enforce {
- background-color: $material-theme-error-palette-default;
-
- .fa {
- color: $primary-contrast;
- }
- }
}
}
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html
index 93a9b035fe..c9ccb9e622 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.html
@@ -15,19 +15,37 @@
~ limitations under the License.
-->
-<h2 mat-dialog-title>
- <div class="flex justify-between items-baseline">
- <div>
- {{ readonly ? 'Processor Details' : 'Edit Processor' }}
+<h2 id="edit-processor-header" mat-dialog-title>
+ <div class="flex justify-between items-center">
+ <div class="flex items-baseline">
+ <div class="mr-2">
+ {{ readonly ? 'Processor Details' : 'Edit Processor' }}
+ </div>
+ |
+ <div class="ml-2 text-base">
+ {{ formatType() }}
+ </div>
</div>
- <div class="text-base">
- {{ formatType(request.entity) }}
+ <div class="flex">
+ @if (hasBulletins()) {
+ <div class="w-[48px]">
+ <div
+ nifiTooltip
+ [delayClose]="true"
+ [tooltipComponentType]="BulletinsTip"
+ [tooltipInputData]="getBulletinsTipData()"
+ [position]="getBulletinTooltipPosition()"
+ [ngClass]="getMostSevereBulletinLevel()"
+ class="absolute top-0 right-0 text-3xl h-14 w-14
bulletins has-bulletins flex justify-center items-center">
+ <i class="fa fa-sticky-note-o"></i>
+ </div>
+ </div>
+ }
</div>
</div>
</h2>
<form class="processor-edit-form" [formGroup]="editProcessorForm">
<context-error-banner
[context]="ErrorContextKey.PROCESSOR"></context-error-banner>
- <!-- TODO - Stop & Configure -->
<mat-tab-group [(selectedIndex)]="selectedIndex"
(selectedIndexChange)="tabChanged($event)">
<mat-tab label="Settings">
<mat-dialog-content>
@@ -48,13 +66,13 @@
<div class="flex flex-col mb-5">
<div>Type</div>
<div class="tertiary-color font-medium">
- {{ formatType(request.entity) }}
+ {{ formatType() }}
</div>
</div>
<div class="flex flex-col mb-6">
<div>Bundle</div>
<div class="tertiary-color font-medium">
- {{ formatBundle(request.entity) }}
+ {{ formatBundle() }}
</div>
</div>
<div class="flex gap-x-4">
@@ -290,19 +308,106 @@
</mat-tab>
</mat-tab-group>
@if ({ value: (saving$ | async)! }; as saving) {
- <mat-dialog-actions align="end">
- @if (readonly) {
- <button mat-flat-button mat-dialog-close>Close</button>
- } @else {
- <button mat-button mat-dialog-close>Cancel</button>
- <button
- [disabled]="!editProcessorForm.dirty ||
editProcessorForm.invalid || saving.value"
- type="button"
- (click)="submitForm()"
- mat-flat-button>
- <span *nifiSpinner="saving.value">Apply</span>
- </button>
- }
+ <mat-dialog-actions align="start">
+ <div class="flex w-full justify-between items-center">
+ <div>
+ @if (isStoppable()) {
+ <button
+ type="button"
+ [disabled]="!canOperate()"
+ mat-stroked-button
+ [matMenuTriggerFor]="operateMenu">
+ <div class="flex items-center">
+ <i class="mr-2 success-color-default fa
fa-play"></i>Running<i
+ class="ml-2 -mt-1 fa fa-sort-desc"></i>
+ </div>
+ </button>
+ } @else if (isRunnable()) {
+ <button
+ type="button"
+ [disabled]="!canOperate()"
+ mat-stroked-button
+ [matMenuTriggerFor]="operateMenu">
+ <div class="flex items-center">
+ <i class="mr-2 error-color-variant fa
fa-stop"></i>Stopped<i
+ class="ml-2 -mt-1 fa fa-sort-desc"></i>
+ </div>
+ </button>
+ } @else if (isDisabled()) {
+ <button
+ type="button"
+ [disabled]="!canOperate()"
+ mat-stroked-button
+ [matMenuTriggerFor]="operateMenu">
+ <div class="flex items-center">
+ <i class="mr-2 icon icon-enable-false
primary-color"></i>Disable<i
+ class="ml-2 -mt-1 fa fa-sort-desc"></i>
+ </div>
+ </button>
+ } @else if (isStopping()) {
+ <button type="button" [disabled]="true"
mat-stroked-button [matMenuTriggerFor]="operateMenu">
+ <div class="flex items-center">
+ <i class="mr-2 fa fa-circle-o-notch fa-spin
primary-color"></i>Stopping ({{
+ status.aggregateSnapshot.activeThreadCount
+ }})
+ </div>
+ </button>
+ } @else if (isValidating()) {
+ <button type="button" [disabled]="true"
mat-stroked-button [matMenuTriggerFor]="operateMenu">
+ <div class="flex items-center">
+ <i class="mr-2 fa fa-circle-o-notch fa-spin
primary-color"></i>Validating
+ </div>
+ </button>
+ } @else if (isInvalid()) {
+ <button
+ type="button"
+ mat-stroked-button
+ [disabled]="!canOperate()"
+ [matMenuTriggerFor]="operateMenu">
+ <div class="flex items-center">
+ <i class="mr-2 fa fa-warning
caution-color"></i>Invalid<i
+ class="ml-2 -mt-1 fa fa-sort-desc"></i>
+ </div>
+ </button>
+ }
+ <mat-menu #operateMenu="matMenu" xPosition="before">
+ @if (isStoppable() && canOperate()) {
+ <button mat-menu-item (click)="stop()">
+ <i class="mr-2 fa fa-stop
primary-color"></i>Stop
+ </button>
+ }
+ @if (isRunnable() && canOperate()) {
+ <button mat-menu-item
[disabled]="editProcessorForm.dirty" (click)="start()">
+ <i class="mr-2 fa fa-play
primary-color"></i>Start
+ </button>
+ }
+ @if (isDisableable() && canOperate()) {
+ <button mat-menu-item
[disabled]="editProcessorForm.dirty" (click)="disable()">
+ <i class="mr-2 icon icon-enable-false
primary-color"></i>Disable
+ </button>
+ }
+ @if (isEnableable() && canOperate()) {
+ <button mat-menu-item
[disabled]="editProcessorForm.dirty" (click)="enable()">
+ <i class="mr-2 fa fa-flash
primary-color"></i>Enable
+ </button>
+ }
+ </mat-menu>
+ </div>
+ <div>
+ @if (readonly) {
+ <button mat-flat-button mat-dialog-close>Close</button>
+ } @else {
+ <button mat-button mat-dialog-close>Cancel</button>
+ <button
+ [disabled]="!editProcessorForm.dirty ||
editProcessorForm.invalid || saving.value"
+ type="button"
+ (click)="submitForm()"
+ mat-flat-button>
+ <span *nifiSpinner="saving.value">Apply</span>
+ </button>
+ }
+ </div>
+ </div>
</mat-dialog-actions>
}
</form>
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts
index b238e9ecc8..b1264b036d 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component.ts
@@ -17,6 +17,7 @@
import { Component, EventEmitter, Inject, Input, Output } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatMenuModule } from '@angular/material/menu';
import {
AbstractControl,
FormBuilder,
@@ -30,19 +31,29 @@ import {
import { MatInputModule } from '@angular/material/input';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
-import { AsyncPipe } from '@angular/common';
+import { AsyncPipe, NgClass } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { Observable, of } from 'rxjs';
import {
+ BulletinEntity,
+ BulletinsTipInput,
InlineServiceCreationRequest,
InlineServiceCreationResponse,
ParameterContextEntity,
- Property
+ Property,
+ Revision
} from '../../../../../../../state/shared';
import { Client } from '../../../../../../../service/client.service';
-import { EditComponentDialogRequest, UpdateProcessorRequest } from
'../../../../../state/flow';
+import {
+ DisableComponentRequest,
+ EditComponentDialogRequest,
+ EnableComponentRequest,
+ StartComponentRequest,
+ StopComponentRequest,
+ UpdateProcessorRequest
+} from '../../../../../state/flow';
import { PropertyTable } from
'../../../../../../../ui/common/property-table/property-table.component';
import { NifiSpinnerDirective } from
'../../../../../../../ui/common/spinner/nifi-spinner.directive';
import { NifiTooltipDirective, NiFiCommon, TextTip, CopyDirective } from
'@nifi/shared';
@@ -51,7 +62,6 @@ import {
RelationshipConfiguration,
RelationshipSettings
} from './relationship-settings/relationship-settings.component';
-import { ErrorBanner } from
'../../../../../../../ui/common/error-banner/error-banner.component';
import { ClusterConnectionService } from
'../../../../../../../service/cluster-connection.service';
import { CanvasUtils } from '../../../../../service/canvas-utils.service';
import { ConvertToParameterResponse } from
'../../../../../service/parameter-helper.service';
@@ -65,6 +75,8 @@ import { TabbedDialog } from
'../../../../../../../ui/common/tabbed-dialog/tabbe
import { ComponentType, SelectOption } from 'libs/shared/src';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from
'../../../../../../../ui/common/context-error-banner/context-error-banner.component';
+import { BulletinsTip } from
'../../../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
+import { ConnectedPosition } from '@angular/cdk/overlay';
@Component({
selector: 'edit-processor',
@@ -79,20 +91,24 @@ import { ContextErrorBanner } from
'../../../../../../../ui/common/context-error
MatTabsModule,
MatOptionModule,
MatSelectModule,
+ MatMenuModule,
AsyncPipe,
PropertyTable,
NifiSpinnerDirective,
NifiTooltipDirective,
RunDurationSlider,
RelationshipSettings,
- ErrorBanner,
PropertyVerification,
ContextErrorBanner,
- CopyDirective
+ CopyDirective,
+ NgClass
],
styleUrls: ['./edit-processor.component.scss']
})
export class EditProcessor extends TabbedDialog {
+ @Input() set processorUpdates(processorUpdates: any | undefined) {
+ this.processRunStateUpdates(processorUpdates);
+ }
@Input() createNewProperty!: (existingProperties: string[],
allowsSensitive: boolean) => Observable<Property>;
@Input() createNewService!: (request: InlineServiceCreationRequest) =>
Observable<InlineServiceCreationResponse>;
@Input() parameterContext: ParameterContextEntity | undefined;
@@ -110,11 +126,20 @@ export class EditProcessor extends TabbedDialog {
@Output() verify: EventEmitter<VerifyPropertiesRequestContext> = new
EventEmitter<VerifyPropertiesRequestContext>();
@Output() editProcessor: EventEmitter<UpdateProcessorRequest> = new
EventEmitter<UpdateProcessorRequest>();
+ @Output() stopComponentRequest: EventEmitter<StopComponentRequest> = new
EventEmitter<StopComponentRequest>();
+ @Output() startComponentRequest: EventEmitter<StartComponentRequest> = new
EventEmitter<StartComponentRequest>();
+ @Output() disableComponentRequest: EventEmitter<DisableComponentRequest> =
+ new EventEmitter<DisableComponentRequest>();
+ @Output() enableComponentRequest: EventEmitter<EnableComponentRequest> =
new EventEmitter<EnableComponentRequest>();
protected readonly TextTip = TextTip;
+ protected readonly BulletinsTip = BulletinsTip;
editProcessorForm: FormGroup;
- readonly: boolean;
+ readonly: boolean = true;
+ status: any;
+ revision!: Revision;
+ bulletins!: BulletinEntity[];
bulletinLevels = [
{
@@ -182,9 +207,6 @@ export class EditProcessor extends TabbedDialog {
) {
super('edit-processor-selected-index');
- this.readonly =
- !request.entity.permissions.canWrite ||
!this.canvasUtils.runnableSupportsModification(request.entity);
-
const processorProperties: any =
request.entity.component.config.properties;
const properties: Property[] =
Object.entries(processorProperties).map((entry: any) => {
const [property, value] = entry;
@@ -253,6 +275,32 @@ export class EditProcessor extends TabbedDialog {
new FormControl({ value: this.runDurationMillis, disabled:
this.readonly }, Validators.required)
);
}
+
+ this.processRunStateUpdates(request.entity);
+ }
+
+ private processRunStateUpdates(entity: any) {
+ this.status = entity.status;
+ this.revision = entity.revision;
+ this.bulletins = entity.bulletins;
+
+ this.readonly = !entity.permissions.canWrite ||
!this.canvasUtils.runnableSupportsModification(entity);
+
+ if (this.readonly) {
+ this.editProcessorForm.get('properties')?.disable();
+ this.editProcessorForm.get('relationshipConfiguration')?.disable();
+
+ if (this.supportsBatching()) {
+ this.editProcessorForm.get('runDuration')?.disable();
+ }
+ } else {
+ this.editProcessorForm.get('properties')?.enable();
+ this.editProcessorForm.get('relationshipConfiguration')?.enable();
+
+ if (this.supportsBatching()) {
+ this.editProcessorForm.get('runDuration')?.enable();
+ }
+ }
}
private relationshipConfigurationValidator(): ValidatorFn {
@@ -288,12 +336,12 @@ export class EditProcessor extends TabbedDialog {
return this.request.entity.component.supportsBatching == true;
}
- formatType(entity: any): string {
- return this.nifiCommon.formatType(entity.component);
+ formatType(): string {
+ return this.nifiCommon.formatType(this.request.entity.component);
}
- formatBundle(entity: any): string {
- return this.nifiCommon.formatBundle(entity.component.bundle);
+ formatBundle(): string {
+ return
this.nifiCommon.formatBundle(this.request.entity.component.bundle);
}
concurrentTasksChanged(): void {
@@ -351,7 +399,10 @@ export class EditProcessor extends TabbedDialog {
.map((relationship) => relationship.name);
const payload: any = {
- revision: this.client.getRevision(this.request.entity),
+ revision: this.client.getRevision({
+ ...this.request.entity,
+ revision: this.revision
+ }),
disconnectedNodeAcknowledged:
this.clusterConnectionService.isDisconnectionAcknowledged(),
component: {
id: this.request.entity.id,
@@ -401,6 +452,140 @@ export class EditProcessor extends TabbedDialog {
});
}
+ hasBulletins(): boolean {
+ return this.request.entity.permissions.canRead &&
!this.nifiCommon.isEmpty(this.bulletins);
+ }
+
+ getBulletinsTipData(): BulletinsTipInput {
+ return {
+ bulletins: this.bulletins
+ };
+ }
+
+ getBulletinTooltipPosition(): ConnectedPosition {
+ return {
+ originX: 'end',
+ originY: 'bottom',
+ overlayX: 'end',
+ overlayY: 'top',
+ offsetX: -8,
+ offsetY: 8
+ };
+ }
+
+ getMostSevereBulletinLevel(): string | null {
+ // determine the most severe of the bulletins
+ const mostSevere =
this.canvasUtils.getMostSevereBulletin(this.bulletins);
+ return mostSevere ? mostSevere.bulletin.level.toLowerCase() : null;
+ }
+
+ isStoppable(): boolean {
+ return this.status.aggregateSnapshot.runStatus === 'Running';
+ }
+
+ isStopping(): boolean {
+ return (
+ this.status.aggregateSnapshot.runStatus === 'Stopped' &&
this.status.aggregateSnapshot.activeThreadCount > 0
+ );
+ }
+
+ isValidating(): boolean {
+ return this.status.aggregateSnapshot.runStatus === 'Validating';
+ }
+
+ isInvalid(): boolean {
+ return this.status.aggregateSnapshot.runStatus === 'Invalid';
+ }
+
+ isDisabled(): boolean {
+ return this.status.aggregateSnapshot.runStatus === 'Disabled';
+ }
+
+ isRunnable(): boolean {
+ return (
+ !(
+ this.status.aggregateSnapshot.runStatus === 'Running' ||
+ this.status.aggregateSnapshot.activeThreadCount > 0
+ ) && this.status.aggregateSnapshot.runStatus === 'Stopped'
+ );
+ }
+
+ isDisableable(): boolean {
+ return (
+ !(
+ this.status.aggregateSnapshot.runStatus === 'Running' ||
+ this.status.aggregateSnapshot.activeThreadCount > 0
+ ) &&
+ (this.status.aggregateSnapshot.runStatus === 'Stopped' ||
+ this.status.aggregateSnapshot.runStatus === 'Invalid')
+ );
+ }
+
+ isEnableable(): boolean {
+ return (
+ !(
+ this.status.aggregateSnapshot.runStatus === 'Running' ||
+ this.status.aggregateSnapshot.activeThreadCount > 0
+ ) && this.status.aggregateSnapshot.runStatus === 'Disabled'
+ );
+ }
+
+ canOperate(): boolean {
+ return this.request.entity.permissions.canWrite ||
this.request.entity.operatePermissions?.canWrite;
+ }
+
+ stop() {
+ this.stopComponentRequest.next({
+ id: this.request.entity.id,
+ uri: this.request.entity.uri,
+ type: ComponentType.Processor,
+ revision: this.client.getRevision({
+ ...this.request.entity,
+ revision: this.revision
+ }),
+ errorStrategy: 'snackbar'
+ });
+ }
+
+ start() {
+ this.startComponentRequest.next({
+ id: this.request.entity.id,
+ uri: this.request.entity.uri,
+ type: ComponentType.Processor,
+ revision: this.client.getRevision({
+ ...this.request.entity,
+ revision: this.revision
+ }),
+ errorStrategy: 'snackbar'
+ });
+ }
+
+ disable() {
+ this.disableComponentRequest.next({
+ id: this.request.entity.id,
+ uri: this.request.entity.uri,
+ type: ComponentType.Processor,
+ revision: this.client.getRevision({
+ ...this.request.entity,
+ revision: this.revision
+ }),
+ errorStrategy: 'snackbar'
+ });
+ }
+
+ enable() {
+ this.enableComponentRequest.next({
+ id: this.request.entity.id,
+ uri: this.request.entity.uri,
+ type: ComponentType.Processor,
+ revision: this.client.getRevision({
+ ...this.request.entity,
+ revision: this.revision
+ }),
+ errorStrategy: 'snackbar'
+ });
+ }
+
private getModifiedProperties(): ModifiedProperties {
const propertyControl: AbstractControl | null =
this.editProcessorForm.get('properties');
if (propertyControl && propertyControl.dirty) {
diff --git
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/service/client.service.ts
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/service/client.service.ts
index 99704c4736..fe2d7167d5 100644
---
a/nifi-frontend/src/main/frontend/apps/nifi/src/app/service/client.service.ts
+++
b/nifi-frontend/src/main/frontend/apps/nifi/src/app/service/client.service.ts
@@ -17,12 +17,24 @@
import { Injectable } from '@angular/core';
import { v4 as uuidv4 } from 'uuid';
+import { SessionStorageService } from '@nifi/shared';
@Injectable({
providedIn: 'root'
})
export class Client {
- private clientId: string = uuidv4();
+ private clientId: string;
+
+ constructor(private sessionStorage: SessionStorageService) {
+ let clientId = this.sessionStorage.getItem<string>('clientId');
+
+ if (clientId === null) {
+ clientId = uuidv4();
+ this.sessionStorage.setItem('clientId', clientId);
+ }
+
+ this.clientId = clientId;
+ }
public getClientId(): string {
return this.clientId;
diff --git a/nifi-frontend/src/main/frontend/apps/nifi/src/styles.scss
b/nifi-frontend/src/main/frontend/apps/nifi/src/styles.scss
index 8ce41675cf..3ae320876c 100644
--- a/nifi-frontend/src/main/frontend/apps/nifi/src/styles.scss
+++ b/nifi-frontend/src/main/frontend/apps/nifi/src/styles.scss
@@ -33,6 +33,7 @@
birdseye-control;
@use
'app/pages/flow-designer/ui/canvas/graph-controls/operation-control/operation-control.component-theme'
as
operation-control;
+@use
'app/pages/flow-designer/ui/canvas/items/processor/edit-processor/edit-processor.component-theme'
as edit-processor;
@use
'app/pages/flow-designer/ui/canvas/header/flow-status/flow-status.component-theme'
as flow-status;
@use
'app/pages/flow-designer/ui/canvas/header/new-canvas-item/new-canvas-item.component-theme'
as new-canvas-item;
@use 'app/pages/flow-designer/ui/canvas/header/search/search.component-theme'
as search;
@@ -94,6 +95,7 @@ html {
@include navigation-control.generate-theme($m3-light-theme,
$m3-light-theme-config);
@include operation-control.generate-theme($m3-light-theme,
$m3-light-theme-config);
@include flow-status.generate-theme($m3-light-theme,
$m3-light-theme-config);
+ @include edit-processor.generate-theme($m3-light-theme,
$m3-light-theme-config);
@include violation-details-dialog.generate-theme($m3-light-theme,
$m3-light-theme-config);
@include new-canvas-item.generate-theme($m3-light-theme,
$m3-light-theme-config);
@include search.generate-theme($m3-light-theme, $m3-light-theme-config);
@@ -127,6 +129,7 @@ html {
@include navigation-control.generate-theme($m3-dark-theme,
$m3-dark-theme-config);
@include operation-control.generate-theme($m3-dark-theme,
$m3-dark-theme-config);
@include flow-status.generate-theme($m3-dark-theme,
$m3-dark-theme-config);
+ @include edit-processor.generate-theme($m3-dark-theme,
$m3-dark-theme-config);
@include violation-details-dialog.generate-theme($m3-dark-theme,
$m3-dark-theme-config);
@include new-canvas-item.generate-theme($m3-dark-theme,
$m3-dark-theme-config);
@include search.generate-theme($m3-dark-theme, $m3-dark-theme-config);
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
b/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
index 58709319c3..89beaac405 100644
--- a/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
+++ b/nifi-frontend/src/main/frontend/libs/shared/src/assets/styles/_app.scss
@@ -161,6 +161,7 @@
--mdc-outlined-button-label-text-tracking: normal;
--mdc-outlined-button-label-text-weight: 400;
--mat-outlined-button-horizontal-padding: 15px;
+ --mdc-outlined-button-container-height: 32px;
}
.mat-mdc-tab-header {
diff --git a/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
index 5e5054972e..8572877ade 100644
--- a/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
+++ b/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
@@ -17,5 +17,6 @@
export * from './nifi-common.service';
export * from './storage.service';
+export * from './session-storage.service';
export * from './theming.service';
export * from './map-table-helper.service';
diff --git a/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.spec.ts
similarity index 65%
copy from nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
copy to
nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.spec.ts
index 5e5054972e..231824f15d 100644
--- a/nifi-frontend/src/main/frontend/libs/shared/src/services/index.ts
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.spec.ts
@@ -15,7 +15,19 @@
* limitations under the License.
*/
-export * from './nifi-common.service';
-export * from './storage.service';
-export * from './theming.service';
-export * from './map-table-helper.service';
+import { TestBed } from '@angular/core/testing';
+
+import { SessionStorageService } from './session-storage.service';
+
+describe('SessionStorageService', () => {
+ let service: SessionStorageService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(SessionStorageService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git
a/nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.ts
b/nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.ts
new file mode 100644
index 0000000000..31d36ac443
--- /dev/null
+++
b/nifi-frontend/src/main/frontend/libs/shared/src/services/session-storage.service.ts
@@ -0,0 +1,110 @@
+/*
+ * 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';
+
+interface SessionStorageEntry<T> {
+ item: T;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class SessionStorageService {
+ /**
+ * Gets an entry for the key. The entry expiration is not checked.
+ *
+ * @param {string} key
+ */
+ private getEntry<T>(key: string): null | SessionStorageEntry<T> {
+ try {
+ // parse the entry
+ const item = sessionStorage.getItem(key);
+ if (!item) {
+ return null;
+ }
+
+ const entry = JSON.parse(item);
+
+ // ensure the entry is present
+ if (entry) {
+ return entry;
+ } else {
+ return null;
+ }
+ } catch (e) {
+ return null;
+ }
+ }
+
+ /**
+ * Stores the specified item.
+ *
+ * @param {string} key
+ * @param {object} item
+ */
+ public setItem<T>(key: string, item: T): void {
+ // create the entry
+ const entry: SessionStorageEntry<T> = {
+ item
+ };
+
+ // store the item
+ sessionStorage.setItem(key, JSON.stringify(entry));
+ }
+
+ /**
+ * Returns whether there is an entry for this key. This will not check the
expiration. If
+ * the entry is expired, it will return null on a subsequent getItem
invocation.
+ *
+ * @param {string} key
+ * @returns {boolean}
+ */
+ public hasItem(key: string): boolean {
+ return this.getEntry(key) !== null;
+ }
+
+ /**
+ * Gets the item with the specified key. If an item with this key does
+ * not exist, null is returned. If an item exists but cannot be parsed
+ * or is malformed/unrecognized, null is returned.
+ *
+ * @param {type} key
+ */
+ public getItem<T>(key: string): null | T {
+ const entry: SessionStorageEntry<T> | null = this.getEntry(key);
+ if (entry === null) {
+ return null;
+ }
+
+ // if the entry has the specified field return its value
+ if (entry['item']) {
+ return entry['item'];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Removes the item with the specified key.
+ *
+ * @param {string} key
+ */
+ public removeItem(key: string): void {
+ sessionStorage.removeItem(key);
+ }
+}