mirror of
https://github.com/apache/nifi.git
synced 2025-02-07 18:48:51 +00:00
NIFI-12445: Provenance Event Listing (#8133)
- Provenance Event Listing. - View Provenance Event dialog. - Provenance routing. - Provenance search. - Replay. - Download content. - View content. - Addressing review feedback. - Addressing review feedback. This closes #8133
This commit is contained in:
parent
78b822c452
commit
4f59f46ce4
@ -16,5 +16,24 @@ export default {
|
||||
headers: {
|
||||
'X-ProxyPort': 4200
|
||||
}
|
||||
},
|
||||
'/nifi-content-viewer/*': {
|
||||
target: 'https://localhost:8443',
|
||||
secure: false,
|
||||
logLevel: 'debug',
|
||||
changeOrigin: true,
|
||||
headers: {
|
||||
'X-ProxyPort': 4200
|
||||
}
|
||||
},
|
||||
// the following entry is needed because the content viewer (and other UIs) load resources from existing nifi ui
|
||||
'/nifi/*': {
|
||||
target: 'https://localhost:8443',
|
||||
secure: false,
|
||||
logLevel: 'debug',
|
||||
changeOrigin: true,
|
||||
headers: {
|
||||
'X-ProxyPort': 4200
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -29,6 +29,11 @@ const routes: Routes = [
|
||||
canMatch: [authGuard],
|
||||
loadChildren: () => import('./pages/settings/feature/settings.module').then((m) => m.SettingsModule)
|
||||
},
|
||||
{
|
||||
path: 'provenance',
|
||||
canMatch: [authGuard],
|
||||
loadChildren: () => import('./pages/provenance/feature/provenance.module').then((m) => m.ProvenanceModule)
|
||||
},
|
||||
{
|
||||
path: 'parameter-contexts',
|
||||
canMatch: [authGuard],
|
||||
|
@ -20,7 +20,6 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { FlowDesignerModule } from './pages/flow-designer/feature/flow-designer.module';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||
@ -29,13 +28,14 @@ import { HTTP_INTERCEPTORS, HttpClientModule, HttpClientXsrfModule } from '@angu
|
||||
import { NavigationActionTiming, RouterState, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||
import { rootReducers } from './state';
|
||||
import { UserEffects } from './state/user/user.effects';
|
||||
import { LoginModule } from './pages/login/feature/login.module';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { LoadingInterceptor } from './service/interceptors/loading.interceptor';
|
||||
import { AuthInterceptor } from './service/interceptors/auth.interceptor';
|
||||
import { ExtensionTypesEffects } from './state/extension-types/extension-types.effects';
|
||||
import { PollingInterceptor } from './service/interceptors/polling.interceptor';
|
||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { AboutEffects } from './state/about/about.effects';
|
||||
|
||||
// @ts-ignore
|
||||
@NgModule({
|
||||
@ -54,13 +54,14 @@ import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||
routerState: RouterState.Minimal,
|
||||
navigationActionTiming: NavigationActionTiming.PostActivation
|
||||
}),
|
||||
EffectsModule.forRoot(UserEffects, ExtensionTypesEffects),
|
||||
EffectsModule.forRoot(UserEffects, ExtensionTypesEffects, AboutEffects),
|
||||
StoreDevtoolsModule.instrument({
|
||||
maxAge: 25,
|
||||
logOnly: environment.production,
|
||||
autoPause: true
|
||||
}),
|
||||
MatProgressSpinnerModule
|
||||
MatProgressSpinnerModule,
|
||||
MatNativeDateModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@ -27,6 +27,8 @@ import { CanvasState } from '../../state';
|
||||
import { transformFeatureKey } from '../../state/transform';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
|
||||
describe('ConnectableBehavior', () => {
|
||||
let service: ConnectableBehavior;
|
||||
@ -46,6 +48,10 @@ describe('ConnectableBehavior', () => {
|
||||
{
|
||||
selector: selectFlowState,
|
||||
value: initialState[flowFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -28,6 +28,8 @@ import { transformFeatureKey } from '../../state/transform';
|
||||
import { selectFlowState } from '../../state/flow/flow.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
|
||||
describe('DraggableBehavior', () => {
|
||||
let service: DraggableBehavior;
|
||||
@ -51,6 +53,10 @@ describe('DraggableBehavior', () => {
|
||||
{
|
||||
selector: selectTransform,
|
||||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -27,6 +27,8 @@ import { transformFeatureKey } from '../../state/transform';
|
||||
import * as fromTransform from '../../state/transform/transform.reducer';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
|
||||
describe('QuickSelectBehavior', () => {
|
||||
let service: QuickSelectBehavior;
|
||||
@ -46,6 +48,10 @@ describe('QuickSelectBehavior', () => {
|
||||
{
|
||||
selector: selectFlowState,
|
||||
value: initialState[flowFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -28,6 +28,8 @@ import { selectFlowState } from '../state/flow/flow.selectors';
|
||||
import { selectTransform } from '../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../state/controller-services';
|
||||
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../state/user/user.reducer';
|
||||
|
||||
describe('BirdseyeView', () => {
|
||||
let service: BirdseyeView;
|
||||
@ -51,6 +53,10 @@ describe('BirdseyeView', () => {
|
||||
{
|
||||
selector: selectTransform,
|
||||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -26,12 +26,15 @@ import {
|
||||
selectCurrentProcessGroupId,
|
||||
selectParentProcessGroupId
|
||||
} from '../state/flow/flow.selectors';
|
||||
import { initialState } from '../state/flow/flow.reducer';
|
||||
import { initialState as initialFlowState } from '../state/flow/flow.reducer';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { BulletinsTip } from '../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
|
||||
import { Position } from '../state/shared';
|
||||
import { ComponentType, Permissions } from '../../../state/shared';
|
||||
import { NiFiCommon } from '../../../service/nifi-common.service';
|
||||
import { User } from '../../../state/user';
|
||||
import { initialState as initialUserState } from '../../../state/user/user.reducer';
|
||||
import { selectUser } from '../../../state/user/user.selectors';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -42,9 +45,10 @@ export class CanvasUtils {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
private trimLengthCaches: Map<string, Map<string, Map<number, number>>> = new Map();
|
||||
private currentProcessGroupId: string = initialState.id;
|
||||
private parentProcessGroupId: string | null = initialState.flow.processGroupFlow.parentGroupId;
|
||||
private canvasPermissions: Permissions = initialState.flow.permissions;
|
||||
private currentProcessGroupId: string = initialFlowState.id;
|
||||
private parentProcessGroupId: string | null = initialFlowState.flow.processGroupFlow.parentGroupId;
|
||||
private canvasPermissions: Permissions = initialFlowState.flow.permissions;
|
||||
private currentUser: User = initialUserState.user;
|
||||
private connections: any[] = [];
|
||||
|
||||
private readonly humanizeDuration: Humanizer;
|
||||
@ -83,6 +87,13 @@ export class CanvasUtils {
|
||||
.subscribe((connections) => {
|
||||
this.connections = connections;
|
||||
});
|
||||
|
||||
this.store
|
||||
.select(selectUser)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((user) => {
|
||||
this.currentUser = user;
|
||||
});
|
||||
}
|
||||
|
||||
public hasDownstream(selection: any): boolean {
|
||||
@ -124,6 +135,13 @@ export class CanvasUtils {
|
||||
d3.select('path.connector').remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current user can access provenance.
|
||||
*/
|
||||
public canAccessProvenance(): boolean {
|
||||
return this.currentUser.provenancePermissions.canRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the specified selection is empty.
|
||||
*
|
||||
@ -435,6 +453,40 @@ export class CanvasUtils {
|
||||
return selection.size() === 1 && selection.classed('funnel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current user can access provenance for the specified component.
|
||||
*
|
||||
* @argument {selection} selection The selection
|
||||
*/
|
||||
public canAccessComponentProvenance(selection: any): boolean {
|
||||
// ensure the correct number of components are selected
|
||||
if (selection.size() !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!this.isLabel(selection) &&
|
||||
!this.isConnection(selection) &&
|
||||
!this.isProcessGroup(selection) &&
|
||||
!this.isRemoteProcessGroup(selection) &&
|
||||
this.canAccessProvenance()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current selection should provide ability to replay latest provenance event.
|
||||
*
|
||||
* @param {selection} selection
|
||||
*/
|
||||
public canReplayComponentProvenance(selection: any): boolean {
|
||||
// ensure the correct number of components are selected
|
||||
if (selection.size() !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isProcessor(selection) && this.canAccessProvenance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently selected components and connections.
|
||||
*
|
||||
|
@ -28,6 +28,8 @@ import { selectFlowState } from '../state/flow/flow.selectors';
|
||||
import { selectTransform } from '../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../state/controller-services';
|
||||
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../state/user/user.reducer';
|
||||
|
||||
describe('CanvasView', () => {
|
||||
let service: CanvasView;
|
||||
@ -51,6 +53,10 @@ describe('CanvasView', () => {
|
||||
{
|
||||
selector: selectTransform,
|
||||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
CreateProcessGroupRequest,
|
||||
CreateProcessorRequest,
|
||||
DeleteComponentRequest,
|
||||
ReplayLastProvenanceEventRequest,
|
||||
Snippet,
|
||||
UpdateComponentRequest,
|
||||
UploadProcessGroupRequest
|
||||
@ -95,10 +96,6 @@ export class FlowService {
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentUser(): Observable<any> {
|
||||
return this.httpClient.get(`${FlowService.API}/flow/controller/bulletins`);
|
||||
}
|
||||
|
||||
getProcessor(id: string): Observable<any> {
|
||||
return this.httpClient.get(`${FlowService.API}/processors/${id}`);
|
||||
}
|
||||
@ -245,4 +242,8 @@ export class FlowService {
|
||||
deleteSnippet(snippetId: string): Observable<any> {
|
||||
return this.httpClient.delete(`${FlowService.API}/snippets/${snippetId}`);
|
||||
}
|
||||
|
||||
replayLastProvenanceEvent(request: ReplayLastProvenanceEventRequest): Observable<any> {
|
||||
return this.httpClient.post(`${FlowService.API}/provenance-events/latest/replays`, request);
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
|
||||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
|
||||
describe('ConnectionManager', () => {
|
||||
let service: ConnectionManager;
|
||||
@ -51,6 +53,10 @@ describe('ConnectionManager', () => {
|
||||
{
|
||||
selector: selectTransform,
|
||||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
|
||||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
|
||||
describe('FunnelManager', () => {
|
||||
let service: FunnelManager;
|
||||
@ -51,6 +53,10 @@ describe('FunnelManager', () => {
|
||||
{
|
||||
selector: selectTransform,
|
||||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -28,6 +28,8 @@ import { selectFlowState } from '../../state/flow/flow.selectors';
|
||||
import { selectTransform } from '../../state/transform/transform.selectors';
|
||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
||||
import { selectUser } from '../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../state/user/user.reducer';
|
||||
|
||||
describe('RemoteProcessGroupManager', () => {
|
||||
let service: RemoteProcessGroupManager;
|
||||
@ -51,6 +53,10 @@ describe('RemoteProcessGroupManager', () => {
|
||||
{
|
||||
selector: selectTransform,
|
||||
value: initialState[transformFeatureKey]
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -53,7 +53,8 @@ import {
|
||||
UpdatePositionsRequest,
|
||||
UploadProcessGroupRequest,
|
||||
EditCurrentProcessGroupRequest,
|
||||
NavigateToControllerServicesRequest
|
||||
NavigateToControllerServicesRequest,
|
||||
ReplayLastProvenanceEventRequest
|
||||
} from './index';
|
||||
|
||||
/*
|
||||
@ -375,3 +376,13 @@ export const renderConnectionsForComponent = createAction(
|
||||
'[Canvas] Render Connections For Component',
|
||||
props<{ id: string; updatePath: boolean; updateLabel: boolean }>()
|
||||
);
|
||||
|
||||
export const navigateToProvenanceForComponent = createAction(
|
||||
'[Canvas] Navigate To Provenance For Component',
|
||||
props<{ id: string }>()
|
||||
);
|
||||
|
||||
export const replayLastProvenanceEvent = createAction(
|
||||
'[Canvas] Replay Last Provenance Event',
|
||||
props<{ request: ReplayLastProvenanceEventRequest }>()
|
||||
);
|
||||
|
@ -1770,6 +1770,94 @@ export class FlowEffects {
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
navigateToProvenanceForComponent$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowActions.navigateToProvenanceForComponent),
|
||||
map((action) => action.id),
|
||||
tap((componentId) => {
|
||||
this.router.navigate(['/provenance'], { queryParams: { componentId } });
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
replayLastProvenanceEvent = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(FlowActions.replayLastProvenanceEvent),
|
||||
map((action) => action.request),
|
||||
tap((request) => {
|
||||
this.flowService.replayLastProvenanceEvent(request).subscribe({
|
||||
next: (response) => {
|
||||
if (response.aggregateSnapshot.failureExplanation) {
|
||||
this.store.dispatch(
|
||||
FlowActions.showOkDialog({
|
||||
title: 'Replay Event Failure',
|
||||
message: response.aggregateSnapshot.failureExplanation
|
||||
})
|
||||
);
|
||||
} else if (response.aggregateSnapshot.eventAvailable !== true) {
|
||||
this.store.dispatch(
|
||||
FlowActions.showOkDialog({
|
||||
title: 'No Event Available',
|
||||
message: 'There was no recent event available to be replayed.'
|
||||
})
|
||||
);
|
||||
} else if (response.nodeSnapshots) {
|
||||
let replayedCount: number = 0;
|
||||
let unavailableCount: number = 0;
|
||||
|
||||
response.nodeSnapshots.forEach((nodeResponse: any) => {
|
||||
if (nodeResponse.snapshot.eventAvailable) {
|
||||
replayedCount++;
|
||||
} else {
|
||||
unavailableCount++;
|
||||
}
|
||||
});
|
||||
|
||||
let message: string;
|
||||
if (unavailableCount === 0) {
|
||||
message = 'All nodes successfully replayed the latest event.';
|
||||
} else {
|
||||
message = `${replayedCount} nodes successfully replayed the latest event but ${unavailableCount} had no recent event available to be replayed.`;
|
||||
}
|
||||
|
||||
this.store.dispatch(
|
||||
FlowActions.showOkDialog({
|
||||
title: 'Events Replayed',
|
||||
message
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
FlowActions.showOkDialog({
|
||||
title: 'Events Replayed',
|
||||
message: 'Successfully replayed the latest event.'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.store.dispatch(
|
||||
FlowActions.loadConnectionsForComponent({
|
||||
id: request.componentId
|
||||
})
|
||||
);
|
||||
},
|
||||
error: (error) => {
|
||||
this.store.dispatch(
|
||||
FlowActions.showOkDialog({
|
||||
title: 'Failed to Replay Event',
|
||||
message: error.error
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
showOkDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
|
@ -304,6 +304,11 @@ export interface NavigateToComponentRequest {
|
||||
processGroupId?: string;
|
||||
}
|
||||
|
||||
export interface ReplayLastProvenanceEventRequest {
|
||||
componentId: string;
|
||||
nodes: string;
|
||||
}
|
||||
|
||||
/*
|
||||
Snippets
|
||||
*/
|
||||
|
@ -30,7 +30,9 @@ import {
|
||||
navigateToControllerServicesForProcessGroup,
|
||||
navigateToEditComponent,
|
||||
navigateToEditCurrentProcessGroup,
|
||||
reloadFlow
|
||||
navigateToProvenanceForComponent,
|
||||
reloadFlow,
|
||||
replayLastProvenanceEvent
|
||||
} from '../../../state/flow/flow.actions';
|
||||
import { CanvasUtils } from '../../../service/canvas-utils.service';
|
||||
import { DeleteComponentRequest, MoveComponentRequest } from '../../../state/flow';
|
||||
@ -154,24 +156,38 @@ export class ContextMenu implements OnInit {
|
||||
menuItems: [
|
||||
{
|
||||
condition: function (canvasUtils: CanvasUtils, selection: any) {
|
||||
// TODO - canReplayProvenance
|
||||
return false;
|
||||
return canvasUtils.canReplayComponentProvenance(selection);
|
||||
},
|
||||
clazz: 'fa',
|
||||
text: 'All nodes',
|
||||
action: function (store: Store<CanvasState>) {
|
||||
// TODO - replayLastAllNodes
|
||||
action: function (store: Store<CanvasState>, selection: any) {
|
||||
const selectionData = selection.datum();
|
||||
store.dispatch(
|
||||
replayLastProvenanceEvent({
|
||||
request: {
|
||||
componentId: selectionData.id,
|
||||
nodes: 'ALL'
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
condition: function (canvasUtils: CanvasUtils, selection: any) {
|
||||
// TODO - canReplayProvenance
|
||||
return false;
|
||||
return canvasUtils.canReplayComponentProvenance(selection);
|
||||
},
|
||||
clazz: 'fa',
|
||||
text: 'Primary node',
|
||||
action: function (store: Store<CanvasState>) {
|
||||
// TODO - replayLastPrimaryNode
|
||||
action: function (store: Store<CanvasState>, selection: any) {
|
||||
const selectionData = selection.datum();
|
||||
store.dispatch(
|
||||
replayLastProvenanceEvent({
|
||||
request: {
|
||||
componentId: selectionData.id,
|
||||
nodes: 'PRIMARY'
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -525,14 +541,18 @@ export class ContextMenu implements OnInit {
|
||||
},
|
||||
{
|
||||
condition: function (canvasUtils: CanvasUtils, selection: any) {
|
||||
// TODO - canAccessProvenance
|
||||
return false;
|
||||
return canvasUtils.canAccessComponentProvenance(selection);
|
||||
},
|
||||
clazz: 'icon icon-provenance',
|
||||
// imgStyle: 'context-menu-provenance',
|
||||
text: 'View data provenance',
|
||||
action: function (store: Store<CanvasState>) {
|
||||
// TODO - openProvenance
|
||||
action: function (store: Store<CanvasState>, selection: any) {
|
||||
const selectionData = selection.datum();
|
||||
store.dispatch(
|
||||
navigateToProvenanceForComponent({
|
||||
id: selectionData.id
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -958,6 +978,7 @@ export class ContextMenu implements OnInit {
|
||||
) {
|
||||
this.allMenus = new Map<string, ContextMenuDefinition>();
|
||||
this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);
|
||||
this.allMenus.set(this.PROVENANCE_REPLAY.id, this.PROVENANCE_REPLAY);
|
||||
this.allMenus.set(this.VERSION_MENU.id, this.VERSION_MENU);
|
||||
this.allMenus.set(this.UPSTREAM_DOWNSTREAM.id, this.UPSTREAM_DOWNSTREAM);
|
||||
this.allMenus.set(this.ALIGN.id, this.ALIGN);
|
||||
|
@ -81,7 +81,11 @@
|
||||
Bulletin Board
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button mat-menu-item class="global-menu-item">
|
||||
<button
|
||||
mat-menu-item
|
||||
class="global-menu-item"
|
||||
[routerLink]="['/provenance']"
|
||||
[disabled]="!user.provenancePermissions.canRead">
|
||||
<i class="icon fa-fw icon-provenance mr-2"></i>
|
||||
Data Provenance
|
||||
</button>
|
||||
|
@ -36,6 +36,8 @@ import { ClusterSummary, ControllerStatus } from '../../../state/flow';
|
||||
import { Search } from './search/search.component';
|
||||
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { selectUser } from '../../../../../state/user/user.selectors';
|
||||
import * as fromUser from '../../../../../state/user/user.reducer';
|
||||
|
||||
describe('HeaderComponent', () => {
|
||||
let component: HeaderComponent;
|
||||
@ -99,6 +101,10 @@ describe('HeaderComponent', () => {
|
||||
{
|
||||
selector: selectControllerBulletins,
|
||||
value: []
|
||||
},
|
||||
{
|
||||
selector: selectUser,
|
||||
value: fromUser.initialState.user
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@ -114,6 +114,7 @@ export class CreateProcessGroup {
|
||||
this.createProcessGroupForm
|
||||
.get('newProcessGroupName')
|
||||
?.setValue(this.nifiCommon.substringBeforeLast(file.name, '.'));
|
||||
this.createProcessGroupForm.get('newProcessGroupName')?.markAsDirty();
|
||||
this.createProcessGroupForm.get('newProcessGroupParameterContext')?.setValue(null);
|
||||
this.flowNameAttached = file.name;
|
||||
this.flowDefinition = file;
|
||||
|
@ -92,8 +92,4 @@ export class ParameterContextService {
|
||||
params: revision
|
||||
});
|
||||
}
|
||||
|
||||
// updateControllerConfig(controllerEntity: ControllerEntity): Observable<any> {
|
||||
// return this.httpClient.put(`${ControllerServiceService.API}/controller/config`, controllerEntity);
|
||||
// }
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 { Provenance } from './provenance.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Provenance,
|
||||
children: [
|
||||
// {
|
||||
// path: 'lineage'
|
||||
// },
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('../ui/provenance-event-listing/provenance-event-listing.module').then(
|
||||
(m) => m.ProvenanceEventListingModule
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ProvenanceRoutingModule {}
|
@ -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.
|
||||
-->
|
||||
|
||||
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
|
||||
<div class="flex justify-between">
|
||||
<h3 class="text-xl bold provenance-header">Provenance</h3>
|
||||
<button class="nifi-button" [routerLink]="['/']">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.provenance-header {
|
||||
color: #728e9b;
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { Provenance } from './provenance.component';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { initialState } from '../state/provenance-event-listing/provenance-event-listing.reducer';
|
||||
|
||||
describe('Provenance', () => {
|
||||
let component: Provenance;
|
||||
let fixture: ComponentFixture<Provenance>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [Provenance],
|
||||
imports: [RouterModule, RouterTestingModule],
|
||||
providers: [
|
||||
provideMockStore({
|
||||
initialState
|
||||
})
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(Provenance);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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, OnInit } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../state';
|
||||
import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
|
||||
import { loadProvenanceOptions } from '../state/provenance-event-listing/provenance-event-listing.actions';
|
||||
import { loadAbout } from '../../../state/about/about.actions';
|
||||
|
||||
@Component({
|
||||
selector: 'provenance',
|
||||
templateUrl: './provenance.component.html',
|
||||
styleUrls: ['./provenance.component.scss']
|
||||
})
|
||||
export class Provenance implements OnInit, OnDestroy {
|
||||
constructor(private store: Store<NiFiState>) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.dispatch(startUserPolling());
|
||||
this.store.dispatch(loadProvenanceOptions());
|
||||
this.store.dispatch(loadAbout());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(stopUserPolling());
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 { StoreModule } from '@ngrx/store';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { Provenance } from './provenance.component';
|
||||
import { ProvenanceRoutingModule } from './provenance-routing.module';
|
||||
import { provenanceFeatureKey, reducers } from '../state';
|
||||
import { ProvenanceEventListingEffects } from '../state/provenance-event-listing/provenance-event-listing.effects';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
|
||||
@NgModule({
|
||||
declarations: [Provenance],
|
||||
exports: [Provenance],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatDialogModule,
|
||||
ProvenanceRoutingModule,
|
||||
StoreModule.forFeature(provenanceFeatureKey, reducers),
|
||||
EffectsModule.forFeature(ProvenanceEventListingEffects)
|
||||
]
|
||||
})
|
||||
export class ProvenanceModule {}
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NiFiCommon } from '../../../service/nifi-common.service';
|
||||
import { ProvenanceRequest } from '../state/provenance-event-listing';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ProvenanceService {
|
||||
private static readonly API: string = '../nifi-api';
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The NiFi model contain the url for each component. That URL is an absolute URL. Angular CSRF handling
|
||||
* does not work on absolute URLs, so we need to strip off the proto for the request header to be added.
|
||||
*
|
||||
* https://stackoverflow.com/a/59586462
|
||||
*
|
||||
* @param url
|
||||
* @private
|
||||
*/
|
||||
private stripProtocol(url: string): string {
|
||||
return this.nifiCommon.substringAfterFirst(url, ':');
|
||||
}
|
||||
|
||||
getSearchOptions(): Observable<any> {
|
||||
return this.httpClient.get(`${ProvenanceService.API}/provenance/search-options`);
|
||||
}
|
||||
|
||||
submitProvenanceQuery(request: ProvenanceRequest): Observable<any> {
|
||||
return this.httpClient.post(`${ProvenanceService.API}/provenance`, { provenance: { request } });
|
||||
}
|
||||
|
||||
getProvenanceQuery(id: string, clusterNodeId?: string): Observable<any> {
|
||||
// TODO - cluster node id
|
||||
return this.httpClient.get(`${ProvenanceService.API}/provenance/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
deleteProvenanceQuery(id: string, clusterNodeId?: string): Observable<any> {
|
||||
// TODO - cluster node id
|
||||
return this.httpClient.delete(`${ProvenanceService.API}/provenance/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
getProvenanceEvent(id: string): Observable<any> {
|
||||
// TODO - cluster node id
|
||||
return this.httpClient.get(`${ProvenanceService.API}/provenance-events/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
downloadContent(id: string, direction: string): void {
|
||||
let dataUri: string = `${ProvenanceService.API}/provenance-events/${encodeURIComponent(
|
||||
id
|
||||
)}/content/${encodeURIComponent(direction)}`;
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
// TODO - cluster node id in query parameters
|
||||
|
||||
if (Object.keys(queryParameters).length > 0) {
|
||||
const query: string = new URLSearchParams(queryParameters).toString();
|
||||
dataUri = `${dataUri}?${query}`;
|
||||
}
|
||||
|
||||
window.open(dataUri);
|
||||
}
|
||||
|
||||
viewContent(nifiUrl: string, contentViewerUrl: string, id: string, direction: string): void {
|
||||
// build the uri to the data
|
||||
let dataUri: string = `${nifiUrl}provenance-events/${encodeURIComponent(id)}/content/${encodeURIComponent(
|
||||
direction
|
||||
)}`;
|
||||
|
||||
const dataUriParameters: any = {};
|
||||
|
||||
// TODO - cluster node id in data uri parameters
|
||||
|
||||
// include parameters if necessary
|
||||
if (Object.keys(dataUriParameters).length > 0) {
|
||||
const dataUriQuery: string = new URLSearchParams(dataUriParameters).toString();
|
||||
dataUri = `${dataUri}?${dataUriQuery}`;
|
||||
}
|
||||
|
||||
// if there's already a query string don't add another ?... this assumes valid
|
||||
// input meaning that if the url has already included a ? it also contains at
|
||||
// least one query parameter
|
||||
let contentViewer: string = contentViewerUrl;
|
||||
if (contentViewer.indexOf('?') === -1) {
|
||||
contentViewer += '?';
|
||||
} else {
|
||||
contentViewer += '&';
|
||||
}
|
||||
|
||||
const contentViewerParameters: any = {
|
||||
ref: dataUri
|
||||
};
|
||||
|
||||
// open the content viewer
|
||||
const contentViewerQuery: string = new URLSearchParams(contentViewerParameters).toString();
|
||||
window.open(`${contentViewer}${contentViewerQuery}`);
|
||||
}
|
||||
|
||||
replay(eventId: string): Observable<any> {
|
||||
const payload: any = {
|
||||
eventId
|
||||
};
|
||||
|
||||
// TODO - add cluster node id in payload
|
||||
|
||||
return this.httpClient.post(`${ProvenanceService.API}/provenance-events/replays`, payload);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Parameter Contexts
|
||||
*/
|
||||
|
||||
import { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
|
||||
import { provenanceEventListingFeatureKey, ProvenanceEventListingState } from './provenance-event-listing';
|
||||
import { provenanceEventListingReducer } from './provenance-event-listing/provenance-event-listing.reducer';
|
||||
|
||||
export const provenanceFeatureKey = 'provenance';
|
||||
|
||||
export interface ProvenanceState {
|
||||
[provenanceEventListingFeatureKey]: ProvenanceEventListingState;
|
||||
}
|
||||
|
||||
export function reducers(state: ProvenanceState | undefined, action: Action) {
|
||||
return combineReducers({
|
||||
[provenanceEventListingFeatureKey]: provenanceEventListingReducer
|
||||
})(state, action);
|
||||
}
|
||||
|
||||
export const selectProvenanceState = createFeatureSelector<ProvenanceState>(provenanceFeatureKey);
|
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 { ProvenanceEventSummary } from '../../../../state/shared';
|
||||
|
||||
export const provenanceEventListingFeatureKey = 'provenanceEventListing';
|
||||
|
||||
export interface ProvenanceOptionsResponse {
|
||||
provenanceOptions: ProvenanceOptions;
|
||||
}
|
||||
|
||||
export interface ProvenanceQueryParams {
|
||||
flowFileUuid?: string;
|
||||
componentId?: string;
|
||||
}
|
||||
|
||||
export interface ProvenanceQueryResponse {
|
||||
provenance: Provenance;
|
||||
}
|
||||
|
||||
export interface SearchableField {
|
||||
field: string;
|
||||
id: string;
|
||||
label: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ProvenanceOptions {
|
||||
searchableFields: SearchableField[];
|
||||
}
|
||||
|
||||
export interface ProvenanceSearchDialogRequest {
|
||||
timeOffset: number;
|
||||
options: ProvenanceOptions;
|
||||
currentRequest: ProvenanceRequest;
|
||||
}
|
||||
|
||||
export interface ProvenanceSearchValue {
|
||||
value: string;
|
||||
inverse: boolean;
|
||||
}
|
||||
|
||||
export interface ProvenanceRequest {
|
||||
searchTerms?: {
|
||||
[key: string]: ProvenanceSearchValue;
|
||||
};
|
||||
clusterNodeId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
minimumFileSize?: string;
|
||||
maximumFileSize?: string;
|
||||
maxResults: number;
|
||||
summarize: boolean;
|
||||
incrementalResults: boolean;
|
||||
}
|
||||
|
||||
export interface ProvenanceResults {
|
||||
provenanceEvents: ProvenanceEventSummary[];
|
||||
total: string;
|
||||
totalCount: number;
|
||||
generated: string;
|
||||
oldestEvent: string;
|
||||
timeOffset: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface Provenance {
|
||||
id: string;
|
||||
uri: string;
|
||||
submissionTime: string;
|
||||
expiration: string;
|
||||
percentCompleted: number;
|
||||
finished: boolean;
|
||||
request: ProvenanceRequest;
|
||||
results: ProvenanceResults;
|
||||
}
|
||||
|
||||
export interface ProvenanceEventListingState {
|
||||
options: ProvenanceOptions | null;
|
||||
request: ProvenanceRequest | null;
|
||||
provenance: Provenance | null;
|
||||
saving: boolean;
|
||||
loadedTimestamp: string;
|
||||
error: string | null;
|
||||
status: 'pending' | 'loading' | 'error' | 'success';
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 { ProvenanceOptionsResponse, ProvenanceQueryResponse, ProvenanceRequest } from './index';
|
||||
|
||||
export const loadProvenanceOptions = createAction('[Provenance Event Listing] Load Provenance Options');
|
||||
|
||||
export const loadProvenanceOptionsSuccess = createAction(
|
||||
'[Provenance Event Listing] Load Provenance Options Success',
|
||||
props<{ response: ProvenanceOptionsResponse }>()
|
||||
);
|
||||
|
||||
export const submitProvenanceQuery = createAction(
|
||||
'[Provenance Event Listing] Submit Provenance Query',
|
||||
props<{ request: ProvenanceRequest }>()
|
||||
);
|
||||
|
||||
export const resubmitProvenanceQuery = createAction(
|
||||
'[Provenance Event Listing] Resubmit Provenance Query',
|
||||
props<{ request: ProvenanceRequest }>()
|
||||
);
|
||||
|
||||
export const submitProvenanceQuerySuccess = createAction(
|
||||
'[Provenance Event Listing] Submit Provenance Query Success',
|
||||
props<{ response: ProvenanceQueryResponse }>()
|
||||
);
|
||||
|
||||
export const startPollingProvenanceQuery = createAction('[Provenance Event Listing] Start Polling Provenance Query');
|
||||
|
||||
export const pollProvenanceQuery = createAction('[Provenance Event Listing] Poll Provenance Query');
|
||||
|
||||
export const pollProvenanceQuerySuccess = createAction(
|
||||
'[Provenance Event Listing] Poll Provenance Query Success',
|
||||
props<{ response: ProvenanceQueryResponse }>()
|
||||
);
|
||||
|
||||
export const stopPollingProvenanceQuery = createAction('[Provenance Event Listing] Stop Polling Provenance Query');
|
||||
|
||||
export const deleteProvenanceQuery = createAction('[Provenance Event Listing] Delete Provenance Query');
|
||||
|
||||
export const provenanceApiError = createAction(
|
||||
'[Provenance Event Listing] Load Parameter Context Listing Error',
|
||||
props<{ error: string }>()
|
||||
);
|
||||
|
||||
export const openProvenanceEventDialog = createAction(
|
||||
'[Provenance Event Listing] Open Provenance Event Dialog',
|
||||
props<{ id: string }>()
|
||||
);
|
||||
|
||||
export const openSearchDialog = createAction('[Provenance Event Listing] Open Search Dialog');
|
||||
|
||||
export const saveProvenanceRequest = createAction(
|
||||
'[Provenance Event Listing] Save Provenance Request',
|
||||
props<{ request: ProvenanceRequest }>()
|
||||
);
|
||||
|
||||
export const clearProvenanceRequest = createAction('[Provenance Event Listing] Clear Provenance Request');
|
||||
|
||||
export const showOkDialog = createAction(
|
||||
'[Provenance Event Listing] Show Ok Dialog',
|
||||
props<{ title: string; message: string }>()
|
||||
);
|
@ -0,0 +1,361 @@
|
||||
/*
|
||||
* 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 ProvenanceEventListingActions from './provenance-event-listing.actions';
|
||||
import {
|
||||
asyncScheduler,
|
||||
catchError,
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
NEVER,
|
||||
of,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
withLatestFrom
|
||||
} from 'rxjs';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NiFiState } from '../../../../state';
|
||||
import { Router } from '@angular/router';
|
||||
import { OkDialog } from '../../../../ui/common/ok-dialog/ok-dialog.component';
|
||||
import { ProvenanceService } from '../../service/provenance.service';
|
||||
import {
|
||||
selectClusterNodeId,
|
||||
selectProvenanceId,
|
||||
selectProvenanceOptions,
|
||||
selectProvenanceRequest,
|
||||
selectTimeOffset
|
||||
} from './provenance-event-listing.selectors';
|
||||
import { Provenance, ProvenanceRequest } from './index';
|
||||
import { ProvenanceSearchDialog } from '../../ui/provenance-event-listing/provenance-search-dialog/provenance-search-dialog.component';
|
||||
import { selectAbout } from '../../../../state/about/about.selectors';
|
||||
import { ProvenanceEventDialog } from '../../../../ui/common/provenance-event-dialog/provenance-event-dialog.component';
|
||||
import { CancelDialog } from '../../../../ui/common/cancel-dialog/cancel-dialog.component';
|
||||
|
||||
@Injectable()
|
||||
export class ProvenanceEventListingEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private store: Store<NiFiState>,
|
||||
private provenanceService: ProvenanceService,
|
||||
private dialog: MatDialog,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
loadProvenanceOptions$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.loadProvenanceOptions),
|
||||
switchMap((request) =>
|
||||
from(this.provenanceService.getSearchOptions()).pipe(
|
||||
map((response) =>
|
||||
ProvenanceEventListingActions.loadProvenanceOptionsSuccess({
|
||||
response
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
ProvenanceEventListingActions.provenanceApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
submitProvenanceQuery$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.submitProvenanceQuery),
|
||||
map((action) => action.request),
|
||||
switchMap((request) =>
|
||||
from(this.provenanceService.submitProvenanceQuery(request)).pipe(
|
||||
map((response) =>
|
||||
ProvenanceEventListingActions.submitProvenanceQuerySuccess({
|
||||
response: {
|
||||
provenance: response.provenance
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) => {
|
||||
this.store.dispatch(
|
||||
ProvenanceEventListingActions.showOkDialog({
|
||||
title: 'Error',
|
||||
message: error.error
|
||||
})
|
||||
);
|
||||
|
||||
return of(
|
||||
ProvenanceEventListingActions.provenanceApiError({
|
||||
error: error.error
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
resubmitProvenanceQuery = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.resubmitProvenanceQuery),
|
||||
map((action) => action.request),
|
||||
switchMap((request) => {
|
||||
const dialogReference = this.dialog.open(CancelDialog, {
|
||||
data: {
|
||||
title: 'Provenance',
|
||||
message: 'Searching provenance events...'
|
||||
},
|
||||
disableClose: true,
|
||||
panelClass: 'small-dialog'
|
||||
});
|
||||
|
||||
dialogReference.componentInstance.cancel.pipe(take(1)).subscribe(() => {
|
||||
this.store.dispatch(ProvenanceEventListingActions.stopPollingProvenanceQuery());
|
||||
});
|
||||
|
||||
return of(ProvenanceEventListingActions.submitProvenanceQuery({ request }));
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
submitProvenanceQuerySuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.submitProvenanceQuerySuccess),
|
||||
map((action) => action.response),
|
||||
switchMap((response) => {
|
||||
const query: Provenance = response.provenance;
|
||||
if (query.finished) {
|
||||
this.dialog.closeAll();
|
||||
return of(ProvenanceEventListingActions.deleteProvenanceQuery());
|
||||
} else {
|
||||
return of(ProvenanceEventListingActions.startPollingProvenanceQuery());
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
startPollingProvenanceQuery$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.startPollingProvenanceQuery),
|
||||
switchMap(() =>
|
||||
interval(2000, asyncScheduler).pipe(
|
||||
takeUntil(this.actions$.pipe(ofType(ProvenanceEventListingActions.stopPollingProvenanceQuery)))
|
||||
)
|
||||
),
|
||||
switchMap(() => of(ProvenanceEventListingActions.pollProvenanceQuery()))
|
||||
)
|
||||
);
|
||||
|
||||
pollProvenanceQuery$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.pollProvenanceQuery),
|
||||
withLatestFrom(this.store.select(selectProvenanceId), this.store.select(selectClusterNodeId)),
|
||||
switchMap(([action, id, clusterNodeId]) => {
|
||||
if (id) {
|
||||
return from(this.provenanceService.getProvenanceQuery(id, clusterNodeId)).pipe(
|
||||
map((response) =>
|
||||
ProvenanceEventListingActions.pollProvenanceQuerySuccess({
|
||||
response: {
|
||||
provenance: response.provenance
|
||||
}
|
||||
})
|
||||
),
|
||||
catchError((error) =>
|
||||
of(
|
||||
ProvenanceEventListingActions.provenanceApiError({
|
||||
error: error.error
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return NEVER;
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
pollProvenanceQuerySuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.pollProvenanceQuerySuccess),
|
||||
map((action) => action.response),
|
||||
switchMap((response) => {
|
||||
const query: Provenance = response.provenance;
|
||||
if (query.finished) {
|
||||
this.dialog.closeAll();
|
||||
return of(ProvenanceEventListingActions.stopPollingProvenanceQuery());
|
||||
} else {
|
||||
return NEVER;
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
stopPollingProvenanceQuery$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.stopPollingProvenanceQuery),
|
||||
switchMap((response) => of(ProvenanceEventListingActions.deleteProvenanceQuery()))
|
||||
)
|
||||
);
|
||||
|
||||
deleteProvenanceQuery$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.deleteProvenanceQuery),
|
||||
withLatestFrom(this.store.select(selectProvenanceId), this.store.select(selectClusterNodeId)),
|
||||
tap(([action, id, clusterNodeId]) => {
|
||||
if (id) {
|
||||
this.provenanceService.deleteProvenanceQuery(id, clusterNodeId).subscribe();
|
||||
}
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
openSearchDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.openSearchDialog),
|
||||
withLatestFrom(
|
||||
this.store.select(selectTimeOffset),
|
||||
this.store.select(selectProvenanceOptions),
|
||||
this.store.select(selectProvenanceRequest),
|
||||
this.store.select(selectAbout)
|
||||
),
|
||||
tap(([request, timeOffset, options, currentRequest, about]) => {
|
||||
if (about) {
|
||||
const dialogReference = this.dialog.open(ProvenanceSearchDialog, {
|
||||
data: {
|
||||
timeOffset,
|
||||
options,
|
||||
currentRequest
|
||||
},
|
||||
panelClass: 'large-dialog'
|
||||
});
|
||||
|
||||
dialogReference.componentInstance.timezone = about.timezone;
|
||||
|
||||
dialogReference.componentInstance.submitSearchCriteria
|
||||
.pipe(take(1))
|
||||
.subscribe((request: ProvenanceRequest) => {
|
||||
if (request.searchTerms) {
|
||||
const queryParams: any = {};
|
||||
if (request.searchTerms['ProcessorID']) {
|
||||
queryParams['componentId'] = request.searchTerms['ProcessorID'].value;
|
||||
}
|
||||
if (request.searchTerms['FlowFileUUID']) {
|
||||
queryParams['flowFileUuid'] = request.searchTerms['FlowFileUUID'].value;
|
||||
}
|
||||
|
||||
// if either of the supported query params are present in the query, update the url
|
||||
if (Object.keys(queryParams).length > 0) {
|
||||
this.router.navigate(['/provenance'], { queryParams });
|
||||
}
|
||||
}
|
||||
|
||||
this.store.dispatch(ProvenanceEventListingActions.saveProvenanceRequest({ request }));
|
||||
});
|
||||
}
|
||||
|
||||
// TODO - if about hasn't loaded we should show an error
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
openProvenanceEventDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.openProvenanceEventDialog),
|
||||
withLatestFrom(this.store.select(selectAbout)),
|
||||
tap(([request, about]) => {
|
||||
this.provenanceService.getProvenanceEvent(request.id).subscribe({
|
||||
next: (response) => {
|
||||
const dialogReference = this.dialog.open(ProvenanceEventDialog, {
|
||||
data: {
|
||||
event: response.provenanceEvent
|
||||
},
|
||||
panelClass: 'large-dialog'
|
||||
});
|
||||
|
||||
dialogReference.componentInstance.contentViewerAvailable =
|
||||
about?.contentViewerUrl != null ?? false;
|
||||
|
||||
dialogReference.componentInstance.downloadContent
|
||||
.pipe(takeUntil(dialogReference.afterClosed()))
|
||||
.subscribe((direction: string) => {
|
||||
this.provenanceService.downloadContent(request.id, direction);
|
||||
});
|
||||
|
||||
if (about) {
|
||||
dialogReference.componentInstance.viewContent
|
||||
.pipe(takeUntil(dialogReference.afterClosed()))
|
||||
.subscribe((direction: string) => {
|
||||
this.provenanceService.viewContent(
|
||||
about.uri,
|
||||
about.contentViewerUrl,
|
||||
request.id,
|
||||
direction
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
dialogReference.componentInstance.replay
|
||||
.pipe(takeUntil(dialogReference.afterClosed()))
|
||||
.subscribe(() => {
|
||||
this.provenanceService.replay(request.id).subscribe(() => {
|
||||
this.store.dispatch(
|
||||
ProvenanceEventListingActions.showOkDialog({
|
||||
title: 'Provenance',
|
||||
message: 'Successfully submitted replay request.'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
// TODO - handle error
|
||||
}
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
showOkDialog$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(ProvenanceEventListingActions.showOkDialog),
|
||||
tap((request) => {
|
||||
this.dialog.open(OkDialog, {
|
||||
data: {
|
||||
title: request.title,
|
||||
message: request.message
|
||||
},
|
||||
panelClass: 'medium-dialog'
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 { ProvenanceEventListingState } from './index';
|
||||
import {
|
||||
clearProvenanceRequest,
|
||||
loadProvenanceOptionsSuccess,
|
||||
pollProvenanceQuerySuccess,
|
||||
provenanceApiError,
|
||||
saveProvenanceRequest,
|
||||
submitProvenanceQuery,
|
||||
submitProvenanceQuerySuccess
|
||||
} from './provenance-event-listing.actions';
|
||||
|
||||
export const initialState: ProvenanceEventListingState = {
|
||||
options: null,
|
||||
request: null,
|
||||
provenance: null,
|
||||
saving: false,
|
||||
loadedTimestamp: '',
|
||||
error: null,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
export const provenanceEventListingReducer = createReducer(
|
||||
initialState,
|
||||
on(loadProvenanceOptionsSuccess, (state, { response }) => ({
|
||||
...state,
|
||||
options: response.provenanceOptions
|
||||
})),
|
||||
on(submitProvenanceQuery, (state) => ({
|
||||
...state,
|
||||
status: 'loading' as const
|
||||
})),
|
||||
on(submitProvenanceQuerySuccess, pollProvenanceQuerySuccess, (state, { response }) => ({
|
||||
...state,
|
||||
provenance: response.provenance,
|
||||
loadedTimestamp: response.provenance.results.generated,
|
||||
error: null,
|
||||
status: 'success' as const
|
||||
})),
|
||||
on(saveProvenanceRequest, (state, { request }) => ({
|
||||
...state,
|
||||
request
|
||||
})),
|
||||
on(clearProvenanceRequest, (state) => ({
|
||||
...state,
|
||||
request: null
|
||||
})),
|
||||
on(provenanceApiError, (state, { error }) => ({
|
||||
...state,
|
||||
saving: false,
|
||||
error,
|
||||
status: 'error' as const
|
||||
}))
|
||||
);
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 { createSelector } from '@ngrx/store';
|
||||
import { ProvenanceState, selectProvenanceState } from '../index';
|
||||
import {
|
||||
Provenance,
|
||||
provenanceEventListingFeatureKey,
|
||||
ProvenanceEventListingState,
|
||||
ProvenanceQueryParams,
|
||||
ProvenanceRequest,
|
||||
ProvenanceResults
|
||||
} from './index';
|
||||
import { selectCurrentRoute } from '../../../../state/router/router.selectors';
|
||||
|
||||
export const selectProvenanceEventListingState = createSelector(
|
||||
selectProvenanceState,
|
||||
(state: ProvenanceState) => state[provenanceEventListingFeatureKey]
|
||||
);
|
||||
|
||||
export const selectSearchableFieldsFromRoute = createSelector(selectCurrentRoute, (route) => {
|
||||
if (route) {
|
||||
const queryParameters: ProvenanceQueryParams = {};
|
||||
if (route.queryParams.componentId) {
|
||||
queryParameters.componentId = route.queryParams.componentId;
|
||||
}
|
||||
if (route.queryParams.flowFileUuid) {
|
||||
queryParameters.flowFileUuid = route.queryParams.flowFileUuid;
|
||||
}
|
||||
return queryParameters;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
export const selectProvenanceRequest = createSelector(
|
||||
selectProvenanceEventListingState,
|
||||
(state: ProvenanceEventListingState) => state.request
|
||||
);
|
||||
|
||||
export const selectStatus = createSelector(
|
||||
selectProvenanceEventListingState,
|
||||
(state: ProvenanceEventListingState) => state.status
|
||||
);
|
||||
|
||||
export const selectProvenanceOptions = createSelector(
|
||||
selectProvenanceEventListingState,
|
||||
(state: ProvenanceEventListingState) => state.options
|
||||
);
|
||||
|
||||
export const selectLoadedTimestamp = createSelector(
|
||||
selectProvenanceEventListingState,
|
||||
(state: ProvenanceEventListingState) => state.loadedTimestamp
|
||||
);
|
||||
|
||||
export const selectProvenance = createSelector(
|
||||
selectProvenanceEventListingState,
|
||||
(state: ProvenanceEventListingState) => state.provenance
|
||||
);
|
||||
|
||||
export const selectProvenanceId = createSelector(selectProvenance, (state: Provenance | null) => state?.id);
|
||||
|
||||
export const selectClusterNodeId = createSelector(
|
||||
selectProvenanceRequest,
|
||||
(state: ProvenanceRequest | null) => state?.clusterNodeId
|
||||
);
|
||||
|
||||
export const selectProvenanceResults = createSelector(selectProvenance, (state: Provenance | null) => state?.results);
|
||||
|
||||
export const selectTimeOffset = createSelector(
|
||||
selectProvenanceResults,
|
||||
(state: ProvenanceResults | undefined) => state?.timeOffset
|
||||
);
|
@ -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 { ProvenanceEventListing } from './provenance-event-listing.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProvenanceEventListing
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ProvenanceEventListingRoutingModule {}
|
@ -0,0 +1,43 @@
|
||||
<!--
|
||||
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
~ contributor license agreements. See the NOTICE file distributed with
|
||||
~ this work for additional information regarding copyright ownership.
|
||||
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
~ (the "License"); you may not use this file except in compliance with
|
||||
~ the License. You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<div class="flex flex-col h-full gap-y-2 text-sm" *ngIf="status$ | async; let status">
|
||||
<div class="flex-1">
|
||||
<ng-container *ngIf="provenance$ | async as provenance; else initialLoading">
|
||||
<provenance-event-table
|
||||
[events]="provenance.results.provenanceEvents"
|
||||
[oldestEventAvailable]="provenance.results.oldestEvent"
|
||||
[resultsMessage]="getResultsMessage(provenance)"
|
||||
[hasRequest]="hasRequest(provenance.request)"
|
||||
(openSearchCriteria)="openSearchCriteria()"
|
||||
(openEventDialog)="openEventDialog($event)"
|
||||
(clearRequest)="clearRequest()"></provenance-event-table>
|
||||
</ng-container>
|
||||
<ng-template #initialLoading>
|
||||
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div class="refresh-container flex items-center gap-x-2">
|
||||
<button class="nifi-button" (click)="refreshParameterContextListing()">
|
||||
<i class="fa fa-refresh" [class.fa-spin]="(status$ | async) === 'loading'"></i>
|
||||
</button>
|
||||
<div>Last updated:</div>
|
||||
<div class="refresh-timestamp">{{ loadedTimestamp$ | async }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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.
|
||||
*/
|
@ -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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProvenanceEventListing } from './provenance-event-listing.component';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { initialState } from '../../state/provenance-event-listing/provenance-event-listing.reducer';
|
||||
|
||||
describe('ParameterContextListing', () => {
|
||||
let component: ProvenanceEventListing;
|
||||
let fixture: ComponentFixture<ProvenanceEventListing>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProvenanceEventListing],
|
||||
providers: [provideMockStore({ initialState })]
|
||||
});
|
||||
fixture = TestBed.createComponent(ProvenanceEventListing);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import {
|
||||
Provenance,
|
||||
ProvenanceEventListingState,
|
||||
ProvenanceRequest,
|
||||
ProvenanceResults
|
||||
} from '../../state/provenance-event-listing';
|
||||
import {
|
||||
selectLoadedTimestamp,
|
||||
selectProvenance,
|
||||
selectProvenanceRequest,
|
||||
selectSearchableFieldsFromRoute,
|
||||
selectStatus
|
||||
} from '../../state/provenance-event-listing/provenance-event-listing.selectors';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { filter, map, take, tap } from 'rxjs';
|
||||
import {
|
||||
clearProvenanceRequest,
|
||||
openProvenanceEventDialog,
|
||||
openSearchDialog,
|
||||
resubmitProvenanceQuery,
|
||||
saveProvenanceRequest
|
||||
} from '../../state/provenance-event-listing/provenance-event-listing.actions';
|
||||
import { ProvenanceSearchDialog } from './provenance-search-dialog/provenance-search-dialog.component';
|
||||
import { ProvenanceEventSummary } from '../../../../state/shared';
|
||||
|
||||
@Component({
|
||||
selector: 'provenance-event-listing',
|
||||
templateUrl: './provenance-event-listing.component.html',
|
||||
styleUrls: ['./provenance-event-listing.component.scss']
|
||||
})
|
||||
export class ProvenanceEventListing {
|
||||
status$ = this.store.select(selectStatus);
|
||||
loadedTimestamp$ = this.store.select(selectLoadedTimestamp);
|
||||
provenance$ = this.store.select(selectProvenance);
|
||||
|
||||
request!: ProvenanceRequest;
|
||||
|
||||
constructor(private store: Store<ProvenanceEventListingState>) {
|
||||
this.store
|
||||
.select(selectSearchableFieldsFromRoute)
|
||||
.pipe(
|
||||
filter((queryParams) => queryParams != null),
|
||||
// only consider the first searchable fields from route, subsequent changes to the route will be present in the saved request
|
||||
take(1),
|
||||
map((queryParams) => {
|
||||
const request: ProvenanceRequest = {
|
||||
incrementalResults: false,
|
||||
maxResults: 1000,
|
||||
summarize: true
|
||||
};
|
||||
|
||||
if (queryParams) {
|
||||
if (queryParams.componentId || queryParams.flowFileUuid) {
|
||||
request.searchTerms = {};
|
||||
|
||||
if (queryParams.componentId) {
|
||||
request.searchTerms['ProcessorID'] = {
|
||||
value: queryParams.componentId,
|
||||
inverse: false
|
||||
};
|
||||
}
|
||||
if (queryParams.flowFileUuid) {
|
||||
request.searchTerms['FlowFileUUID'] = {
|
||||
value: queryParams.flowFileUuid,
|
||||
inverse: false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
})
|
||||
)
|
||||
.subscribe((request) => {
|
||||
this.store.dispatch(
|
||||
saveProvenanceRequest({
|
||||
request
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// any changes to the saved request will trigger a provenance query submission
|
||||
this.store
|
||||
.select(selectProvenanceRequest)
|
||||
.pipe(
|
||||
map((request) => {
|
||||
if (request) {
|
||||
return request;
|
||||
}
|
||||
|
||||
const initialRequest: ProvenanceRequest = {
|
||||
incrementalResults: false,
|
||||
maxResults: 1000,
|
||||
summarize: true
|
||||
};
|
||||
|
||||
return initialRequest;
|
||||
}),
|
||||
tap((request) => (this.request = request)),
|
||||
takeUntilDestroyed()
|
||||
)
|
||||
.subscribe((request) => {
|
||||
this.store.dispatch(
|
||||
resubmitProvenanceQuery({
|
||||
request
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getResultsMessage(provenance: Provenance): string {
|
||||
const request: ProvenanceRequest = provenance.request;
|
||||
const results: ProvenanceResults = provenance.results;
|
||||
|
||||
if (this.hasRequest(request)) {
|
||||
if (results.totalCount >= ProvenanceSearchDialog.MAX_RESULTS) {
|
||||
return `Showing ${ProvenanceSearchDialog.MAX_RESULTS} of ${results.totalCount} events that match the specified query, please refine the search.`;
|
||||
} else {
|
||||
return 'Showing the events that match the specified query.';
|
||||
}
|
||||
} else {
|
||||
if (results.totalCount >= ProvenanceSearchDialog.MAX_RESULTS) {
|
||||
return `Showing the most recent ${ProvenanceSearchDialog.MAX_RESULTS} of ${results.totalCount} events, please refine the search.`;
|
||||
} else {
|
||||
return 'Showing the most recent events.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasRequest(request: ProvenanceRequest): boolean {
|
||||
const hasSearchTerms: boolean = !!request.searchTerms && Object.keys(request.searchTerms).length > 0;
|
||||
return !!request.startDate || !!request.endDate || hasSearchTerms;
|
||||
}
|
||||
|
||||
clearRequest(): void {
|
||||
this.store.dispatch(clearProvenanceRequest());
|
||||
}
|
||||
|
||||
openSearchCriteria(): void {
|
||||
this.store.dispatch(openSearchDialog());
|
||||
}
|
||||
|
||||
openEventDialog(event: ProvenanceEventSummary): void {
|
||||
this.store.dispatch(
|
||||
openProvenanceEventDialog({
|
||||
id: event.id
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
refreshParameterContextListing(): void {
|
||||
this.store.dispatch(
|
||||
resubmitProvenanceQuery({
|
||||
request: this.request
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -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 { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ProvenanceEventListing } from './provenance-event-listing.component';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { NifiTooltipDirective } from '../../../../ui/common/tooltips/nifi-tooltip.directive';
|
||||
import { ProvenanceEventListingRoutingModule } from './provenance-event-listing-routing.module';
|
||||
import { ProvenanceEventTable } from './provenance-event-table/provenance-event-table.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ProvenanceEventListing],
|
||||
exports: [ProvenanceEventListing],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ProvenanceEventListingRoutingModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NifiTooltipDirective,
|
||||
ProvenanceEventTable
|
||||
]
|
||||
})
|
||||
export class ProvenanceEventListingModule {}
|
@ -0,0 +1,156 @@
|
||||
<!--
|
||||
~ 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="provenance-event-table h-full flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<div class="value font-bold">Displaying {{ filteredCount }} of {{ totalCount }}</div>
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
Oldest event available: <span class="value">{{ oldestEventAvailable }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ resultsMessage }}
|
||||
<a *ngIf="hasRequest" (click)="clearRequestClicked()">Clear Search</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<form [formGroup]="filterForm">
|
||||
<div class="flex pt-2">
|
||||
<div class="mr-2">
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<input matInput type="text" class="small" formControlName="filterTerm" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field>
|
||||
<mat-label>Filter By</mat-label>
|
||||
<mat-select formControlName="filterColumn">
|
||||
<mat-option *ngFor="let option of filterColumnOptions" [value]="option"
|
||||
>{{ option }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex flex-col justify-center">
|
||||
<button class="nifi-button" (click)="searchClicked()">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 relative">
|
||||
<div class="listing-table border absolute inset-0 overflow-y-auto">
|
||||
<table
|
||||
mat-table
|
||||
[dataSource]="dataSource"
|
||||
matSort
|
||||
matSortDisableClear
|
||||
(matSortChange)="updateSort($event)"
|
||||
[matSortActive]="sort.active"
|
||||
[matSortDirection]="sort.direction">
|
||||
<!-- More Details Column -->
|
||||
<ng-container matColumnDef="moreDetails">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div
|
||||
class="pointer fa fa-info-circle"
|
||||
title="View Details"
|
||||
(click)="viewDetailsClicked(item)"></div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="eventTime">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Event Time</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.eventTime }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Type Column -->
|
||||
<ng-container matColumnDef="eventType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Type</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.eventType }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- FlowFile UUID Column -->
|
||||
<ng-container matColumnDef="flowFileUuid">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>FlowFile UUID</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.flowFileUuid }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- File Size Column -->
|
||||
<ng-container matColumnDef="fileSize">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>File Size</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.fileSize }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Component Name Column -->
|
||||
<ng-container matColumnDef="componentName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Component Name</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.componentName }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Component Type Column -->
|
||||
<ng-container matColumnDef="componentType">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>Component Type</th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
{{ item.componentType }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Actions Column -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef></th>
|
||||
<td mat-cell *matCellDef="let item">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<!-- <div-->
|
||||
<!-- class="pointer icon icon-lineage"-->
|
||||
<!-- *ngIf="canStop(item)"-->
|
||||
<!-- (click)="stopClicked(item)"-->
|
||||
<!-- title="Stop"></div>-->
|
||||
<div
|
||||
*ngIf="supportsGoTo(item)"
|
||||
class="pointer fa fa-long-arrow-right"
|
||||
title="Go To"
|
||||
[routerLink]="getComponentLink(item)"></div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; let even = even; columns: displayedColumns"
|
||||
(click)="select(row)"
|
||||
[class.selected]="isSelected(row)"
|
||||
[class.even]="even"></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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.
|
||||
*/
|
||||
|
||||
.provenance-event-table {
|
||||
.listing-table {
|
||||
table {
|
||||
.mat-column-actions {
|
||||
min-width: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { ProvenanceEventTable } from './provenance-event-table.component';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('ProvenanceEventTable', () => {
|
||||
let component: ProvenanceEventTable;
|
||||
let fixture: ComponentFixture<ProvenanceEventTable>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ProvenanceEventTable, MatTableModule, BrowserAnimationsModule]
|
||||
});
|
||||
fixture = TestBed.createComponent(ProvenanceEventTable);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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 { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule, Sort } from '@angular/material/sort';
|
||||
import { TextTip } from '../../../../../ui/common/tooltips/text-tip/text-tip.component';
|
||||
import { BulletinsTip } from '../../../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
|
||||
import { ValidationErrorsTip } from '../../../../../ui/common/tooltips/validation-errors-tip/validation-errors-tip.component';
|
||||
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NgForOf, NgIf } from '@angular/common';
|
||||
import { debounceTime } from 'rxjs';
|
||||
import { ProvenanceEventSummary } from '../../../../../state/shared';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
@Component({
|
||||
selector: 'provenance-event-table',
|
||||
standalone: true,
|
||||
templateUrl: './provenance-event-table.component.html',
|
||||
imports: [
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatOptionModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
NgForOf,
|
||||
NgIf,
|
||||
RouterLink,
|
||||
NgxSkeletonLoaderModule
|
||||
],
|
||||
styleUrls: ['./provenance-event-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
|
||||
})
|
||||
export class ProvenanceEventTable implements AfterViewInit {
|
||||
@Input() set events(events: ProvenanceEventSummary[]) {
|
||||
if (events) {
|
||||
this.dataSource.data = this.sortEvents(events, this.sort);
|
||||
this.dataSource.filterPredicate = (data: ProvenanceEventSummary, filter: string) => {
|
||||
const filterArray = filter.split('|');
|
||||
const filterTerm = filterArray[0];
|
||||
const filterColumn = filterArray[1];
|
||||
|
||||
if (filterColumn === this.filterColumnOptions[0]) {
|
||||
return data.componentName.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
|
||||
} else if (filterColumn === this.filterColumnOptions[1]) {
|
||||
return data.componentType.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
|
||||
} else {
|
||||
return data.eventType.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
|
||||
}
|
||||
};
|
||||
this.totalCount = events.length;
|
||||
this.filteredCount = events.length;
|
||||
|
||||
// apply any filtering to the new data
|
||||
const filterTerm = this.filterForm.get('filterTerm')?.value;
|
||||
if (filterTerm?.length > 0) {
|
||||
const filterColumn = this.filterForm.get('filterColumn')?.value;
|
||||
this.applyFilter(filterTerm, filterColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Input() oldestEventAvailable!: string;
|
||||
@Input() resultsMessage!: string;
|
||||
@Input() hasRequest!: boolean;
|
||||
|
||||
@Output() openSearchCriteria: EventEmitter<void> = new EventEmitter<void>();
|
||||
@Output() clearRequest: EventEmitter<void> = new EventEmitter<void>();
|
||||
@Output() openEventDialog: EventEmitter<ProvenanceEventSummary> = new EventEmitter<ProvenanceEventSummary>();
|
||||
|
||||
protected readonly TextTip = TextTip;
|
||||
protected readonly BulletinsTip = BulletinsTip;
|
||||
protected readonly ValidationErrorsTip = ValidationErrorsTip;
|
||||
|
||||
// TODO - conditionally include the cluster column
|
||||
displayedColumns: string[] = [
|
||||
'moreDetails',
|
||||
'eventTime',
|
||||
'eventType',
|
||||
'flowFileUuid',
|
||||
'fileSize',
|
||||
'componentName',
|
||||
'componentType',
|
||||
'actions'
|
||||
];
|
||||
dataSource: MatTableDataSource<ProvenanceEventSummary> = new MatTableDataSource<ProvenanceEventSummary>();
|
||||
selectedEventId: string | null = null;
|
||||
|
||||
sort: Sort = {
|
||||
active: 'eventTime',
|
||||
direction: 'desc'
|
||||
};
|
||||
|
||||
filterForm: FormGroup;
|
||||
filterColumnOptions: string[] = ['component name', 'component type', 'type'];
|
||||
totalCount: number = 0;
|
||||
filteredCount: number = 0;
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {
|
||||
this.filterForm = this.formBuilder.group({ filterTerm: '', filterColumn: this.filterColumnOptions[0] });
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.filterForm
|
||||
.get('filterTerm')
|
||||
?.valueChanges.pipe(debounceTime(500))
|
||||
.subscribe((filterTerm: string) => {
|
||||
const filterColumn = this.filterForm.get('filterColumn')?.value;
|
||||
this.applyFilter(filterTerm, filterColumn);
|
||||
});
|
||||
|
||||
this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
|
||||
const filterTerm = this.filterForm.get('filterTerm')?.value;
|
||||
this.applyFilter(filterTerm, filterColumn);
|
||||
});
|
||||
}
|
||||
|
||||
updateSort(sort: Sort): void {
|
||||
this.sort = sort;
|
||||
this.dataSource.data = this.sortEvents(this.dataSource.data, sort);
|
||||
}
|
||||
|
||||
sortEvents(events: ProvenanceEventSummary[], sort: Sort): ProvenanceEventSummary[] {
|
||||
const data: ProvenanceEventSummary[] = events.slice();
|
||||
return data.sort((a, b) => {
|
||||
const isAsc = sort.direction === 'asc';
|
||||
|
||||
switch (sort.active) {
|
||||
case 'eventTime':
|
||||
// event ideas are increasing, so we can use this simple number for sorting purposes
|
||||
// since we don't surface the timestamp as millis
|
||||
return (a.eventId - b.eventId) * (isAsc ? 1 : -1);
|
||||
case 'eventType':
|
||||
return this.compare(a.eventType, b.eventType, isAsc);
|
||||
case 'flowFileUuid':
|
||||
return this.compare(a.flowFileUuid, b.flowFileUuid, isAsc);
|
||||
case 'fileSize':
|
||||
return (a.fileSizeBytes - b.fileSizeBytes) * (isAsc ? 1 : -1);
|
||||
case 'componentName':
|
||||
return this.compare(a.componentName, b.componentName, isAsc);
|
||||
case 'componentType':
|
||||
return this.compare(a.componentType, b.componentType, isAsc);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private compare(a: string, b: string, isAsc: boolean): number {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
|
||||
}
|
||||
|
||||
applyFilter(filterTerm: string, filterColumn: string) {
|
||||
this.dataSource.filter = `${filterTerm}|${filterColumn}`;
|
||||
this.filteredCount = this.dataSource.filteredData.length;
|
||||
}
|
||||
|
||||
clearRequestClicked(): void {
|
||||
this.clearRequest.next();
|
||||
}
|
||||
|
||||
searchClicked() {
|
||||
this.openSearchCriteria.next();
|
||||
}
|
||||
|
||||
viewDetailsClicked(event: ProvenanceEventSummary) {
|
||||
this.openEventDialog.next(event);
|
||||
}
|
||||
|
||||
select(event: ProvenanceEventSummary): void {
|
||||
this.selectedEventId = event.id;
|
||||
}
|
||||
|
||||
isSelected(event: ProvenanceEventSummary): boolean {
|
||||
if (this.selectedEventId) {
|
||||
return event.id == this.selectedEventId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
supportsGoTo(event: ProvenanceEventSummary): boolean {
|
||||
if (event.groupId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.componentId === 'Remote Output Port' || event.componentId === 'Remote Input Port') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getComponentLink(event: ProvenanceEventSummary): string[] {
|
||||
let link: string[];
|
||||
|
||||
if (event.groupId == event.componentId) {
|
||||
link = ['/process-groups', event.componentId];
|
||||
} else if (event.componentId === 'Connection' || event.componentId === 'Load Balanced Connection') {
|
||||
link = ['/process-groups', event.groupId, 'Connection', event.componentId];
|
||||
} else if (event.componentId === 'Output Port') {
|
||||
link = ['/process-groups', event.groupId, 'OutputPort', event.componentId];
|
||||
} else {
|
||||
link = ['/process-groups', event.groupId, 'Processor', event.componentId];
|
||||
}
|
||||
|
||||
return link;
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title>Search Events</h2>
|
||||
<form class="search-events-form" [formGroup]="provenanceOptionsForm">
|
||||
<mat-dialog-content>
|
||||
<ng-container *ngFor="let searchableField of request.options.searchableFields">
|
||||
<div [formGroupName]="searchableField.id">
|
||||
<mat-form-field>
|
||||
<mat-label>{{ searchableField.label }}</mat-label>
|
||||
<input matInput formControlName="value" type="text" />
|
||||
</mat-form-field>
|
||||
<div class="-mt-6 mb-4">
|
||||
<mat-checkbox formControlName="inverse" name="inverse"> Exclude </mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<mat-form-field>
|
||||
<mat-label>Date Range</mat-label>
|
||||
<mat-date-range-input [rangePicker]="picker">
|
||||
<input matStartDate formControlName="startDate" placeholder="Start date" />
|
||||
<input matEndDate formControlName="endDate" placeholder="End date" />
|
||||
</mat-date-range-input>
|
||||
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-date-range-picker #picker></mat-date-range-picker>
|
||||
</mat-form-field>
|
||||
<div class="flex gap-x-3">
|
||||
<div class="w-full flex flex-col">
|
||||
<mat-form-field>
|
||||
<mat-label>Start Time ({{ timezone }})</mat-label>
|
||||
<input matInput type="text" formControlName="startTime" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Minimum File Size</mat-label>
|
||||
<input matInput type="text" formControlName="minFileSize" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="w-full flex flex-col">
|
||||
<mat-form-field>
|
||||
<mat-label>End Time ({{ timezone }})</mat-label>
|
||||
<input matInput type="text" formControlName="endTime" />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-label>Maximum File Size</mat-label>
|
||||
<input matInput type="text" formControlName="maxFileSize" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button color="accent" mat-raised-button mat-dialog-close>Cancel</button>
|
||||
<button
|
||||
[disabled]="provenanceOptionsForm.invalid"
|
||||
type="button"
|
||||
color="primary"
|
||||
(click)="searchClicked()"
|
||||
mat-raised-button
|
||||
mat-dialog-close>
|
||||
<span>Search</span>
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
@ -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.
|
||||
*/
|
||||
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.search-events-form {
|
||||
@include mat.button-density(-1);
|
||||
|
||||
.mat-mdc-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mdc-text-field__input::-webkit-calendar-picker-indicator {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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 { ProvenanceSearchDialog } from './provenance-search-dialog.component';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
|
||||
describe('ProvenanceSearchDialog', () => {
|
||||
let component: ProvenanceSearchDialog;
|
||||
let fixture: ComponentFixture<ProvenanceSearchDialog>;
|
||||
|
||||
const data: any = {
|
||||
timeOffset: -18000000,
|
||||
options: {
|
||||
searchableFields: [
|
||||
{
|
||||
id: 'EventType',
|
||||
field: 'eventType',
|
||||
label: 'Event Type',
|
||||
type: 'STRING'
|
||||
},
|
||||
{
|
||||
id: 'FlowFileUUID',
|
||||
field: 'uuid',
|
||||
label: 'FlowFile UUID',
|
||||
type: 'STRING'
|
||||
},
|
||||
{
|
||||
id: 'Filename',
|
||||
field: 'filename',
|
||||
label: 'Filename',
|
||||
type: 'STRING'
|
||||
},
|
||||
{
|
||||
id: 'ProcessorID',
|
||||
field: 'processorId',
|
||||
label: 'Component ID',
|
||||
type: 'STRING'
|
||||
},
|
||||
{
|
||||
id: 'Relationship',
|
||||
field: 'relationship',
|
||||
label: 'Relationship',
|
||||
type: 'STRING'
|
||||
}
|
||||
]
|
||||
},
|
||||
currentRequest: {
|
||||
incrementalResults: false,
|
||||
maxResults: 1000,
|
||||
summarize: true
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ProvenanceSearchDialog, BrowserAnimationsModule, MatNativeDateModule],
|
||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
|
||||
});
|
||||
fixture = TestBed.createComponent(ProvenanceSearchDialog);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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, EventEmitter, Inject, Input, Output } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
|
||||
import {
|
||||
ProvenanceRequest,
|
||||
ProvenanceSearchDialogRequest,
|
||||
SearchableField
|
||||
} from '../../../state/provenance-event-listing';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||
|
||||
@Component({
|
||||
selector: 'provenance-search-dialog',
|
||||
standalone: true,
|
||||
templateUrl: './provenance-search-dialog.component.html',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
NgForOf,
|
||||
MatDatepickerModule
|
||||
],
|
||||
styleUrls: ['./provenance-search-dialog.component.scss']
|
||||
})
|
||||
export class ProvenanceSearchDialog {
|
||||
@Input() timezone!: string;
|
||||
@Output() submitSearchCriteria: EventEmitter<ProvenanceRequest> = new EventEmitter<ProvenanceRequest>();
|
||||
|
||||
public static readonly MAX_RESULTS: number = 1000;
|
||||
private static readonly DEFAULT_START_TIME: string = '00:00:00';
|
||||
private static readonly DEFAULT_END_TIME: string = '23:59:59';
|
||||
|
||||
provenanceOptionsForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public request: ProvenanceSearchDialogRequest,
|
||||
private formBuilder: FormBuilder,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {
|
||||
const now = new Date();
|
||||
this.clearTime(now);
|
||||
|
||||
let startDate: Date = now;
|
||||
let startTime: string = ProvenanceSearchDialog.DEFAULT_START_TIME;
|
||||
let endDate: Date = now;
|
||||
let endTime: string = ProvenanceSearchDialog.DEFAULT_END_TIME;
|
||||
let minFileSize: string = '';
|
||||
let maxFileSize: string = '';
|
||||
|
||||
if (request.currentRequest) {
|
||||
const requestedStartDate = request.currentRequest.startDate;
|
||||
if (requestedStartDate) {
|
||||
startDate = this.nifiCommon.parseDateTime(requestedStartDate);
|
||||
this.clearTime(startDate);
|
||||
|
||||
const requestedStartDateTime = requestedStartDate?.split(' ');
|
||||
if (requestedStartDateTime && requestedStartDateTime.length > 1) {
|
||||
startTime = requestedStartDateTime[1];
|
||||
}
|
||||
}
|
||||
|
||||
const requestedEndDate = request.currentRequest.endDate;
|
||||
if (requestedEndDate) {
|
||||
endDate = this.nifiCommon.parseDateTime(requestedEndDate);
|
||||
this.clearTime(endDate);
|
||||
|
||||
const requestedEndDateTime = requestedEndDate?.split(' ');
|
||||
if (requestedEndDateTime && requestedEndDateTime.length > 0) {
|
||||
endTime = requestedEndDateTime[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (request.currentRequest.minimumFileSize) {
|
||||
minFileSize = request.currentRequest.minimumFileSize;
|
||||
}
|
||||
|
||||
if (request.currentRequest.maximumFileSize) {
|
||||
maxFileSize = request.currentRequest.maximumFileSize;
|
||||
}
|
||||
}
|
||||
|
||||
this.provenanceOptionsForm = this.formBuilder.group({
|
||||
startDate: new FormControl(startDate),
|
||||
startTime: new FormControl(startTime),
|
||||
endDate: new FormControl(endDate),
|
||||
endTime: new FormControl(endTime),
|
||||
minFileSize: new FormControl(minFileSize),
|
||||
maxFileSize: new FormControl(maxFileSize)
|
||||
});
|
||||
|
||||
const searchTerms: any = request.currentRequest?.searchTerms;
|
||||
|
||||
const searchableFields: SearchableField[] = request.options.searchableFields;
|
||||
searchableFields.forEach((searchableField) => {
|
||||
let value: string = '';
|
||||
let inverse: boolean = false;
|
||||
if (searchTerms && searchTerms[searchableField.id]) {
|
||||
value = searchTerms[searchableField.id].value;
|
||||
inverse = searchTerms[searchableField.id].inverse;
|
||||
}
|
||||
|
||||
this.provenanceOptionsForm.addControl(
|
||||
searchableField.id,
|
||||
this.formBuilder.group({
|
||||
value: new FormControl(value),
|
||||
inverse: new FormControl(inverse)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private clearTime(date: Date): void {
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
}
|
||||
|
||||
searchClicked() {
|
||||
const provenanceRequest: ProvenanceRequest = {
|
||||
maxResults: ProvenanceSearchDialog.MAX_RESULTS,
|
||||
summarize: true,
|
||||
incrementalResults: false
|
||||
};
|
||||
|
||||
const startDate: Date = this.provenanceOptionsForm.get('startDate')?.value;
|
||||
if (startDate) {
|
||||
// convert the date object into the format the api expects
|
||||
const formatted: string = this.nifiCommon.formatDateTime(startDate);
|
||||
|
||||
// get just the date portion because the time is entered separately by the user
|
||||
const formattedStartDateTime = formatted.split(' ');
|
||||
if (formattedStartDateTime.length > 0) {
|
||||
const formattedStartDate = formattedStartDateTime[0];
|
||||
|
||||
let startTime: string = this.provenanceOptionsForm.get('startTime')?.value;
|
||||
if (!startTime) {
|
||||
startTime = ProvenanceSearchDialog.DEFAULT_START_TIME;
|
||||
}
|
||||
|
||||
// combine all three pieces into the format the api requires
|
||||
provenanceRequest.startDate = `${formattedStartDate} ${startTime} ${this.timezone}`;
|
||||
}
|
||||
}
|
||||
|
||||
const endDate: Date = this.provenanceOptionsForm.get('endDate')?.value;
|
||||
if (endDate) {
|
||||
// convert the date object into the format the api expects
|
||||
const formatted: string = this.nifiCommon.formatDateTime(endDate);
|
||||
|
||||
// get just the date portion because the time is entered separately by the user
|
||||
const formattedEndDateTime = formatted.split(' ');
|
||||
if (formattedEndDateTime.length > 0) {
|
||||
const formattedEndDate = formattedEndDateTime[0];
|
||||
|
||||
let endTime: string = this.provenanceOptionsForm.get('endTime')?.value;
|
||||
if (!endTime) {
|
||||
endTime = ProvenanceSearchDialog.DEFAULT_END_TIME;
|
||||
}
|
||||
|
||||
// combine all three pieces into the format the api requires
|
||||
provenanceRequest.endDate = `${formattedEndDate} ${endTime} ${this.timezone}`;
|
||||
}
|
||||
}
|
||||
|
||||
const minFileSize: string = this.provenanceOptionsForm.get('minFileSize')?.value;
|
||||
if (minFileSize) {
|
||||
provenanceRequest.minimumFileSize = minFileSize;
|
||||
}
|
||||
|
||||
const maxFileSize: string = this.provenanceOptionsForm.get('maxFileSize')?.value;
|
||||
if (maxFileSize) {
|
||||
provenanceRequest.maximumFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
const searchTerms: any = {};
|
||||
const searchableFields: SearchableField[] = this.request.options.searchableFields;
|
||||
searchableFields.forEach((searchableField) => {
|
||||
// @ts-ignore
|
||||
const searchableFieldForm: FormGroup = this.provenanceOptionsForm.get(searchableField.id);
|
||||
if (searchableFieldForm) {
|
||||
const searchableFieldValue: string = searchableFieldForm.get('value')?.value;
|
||||
if (searchableFieldValue) {
|
||||
searchTerms[searchableField.id] = {
|
||||
value: searchableFieldValue,
|
||||
inverse: searchableFieldForm.get('inverse')?.value
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
provenanceRequest.searchTerms = searchTerms;
|
||||
|
||||
this.submitSearchCriteria.next(provenanceRequest);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AboutService {
|
||||
private static readonly API: string = '../nifi-api';
|
||||
|
||||
constructor(private httpClient: HttpClient) {}
|
||||
|
||||
getAbout(): Observable<any> {
|
||||
return this.httpClient.get(`${AboutService.API}/flow/about`);
|
||||
}
|
||||
}
|
@ -21,6 +21,11 @@ import { Injectable } from '@angular/core';
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class NiFiCommon {
|
||||
private static readonly MILLIS_PER_DAY: number = 86400000;
|
||||
private static readonly MILLIS_PER_HOUR: number = 3600000;
|
||||
private static readonly MILLIS_PER_MINUTE: number = 60000;
|
||||
private static readonly MILLIS_PER_SECOND: number = 1000;
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
@ -171,4 +176,144 @@ export class NiFiCommon {
|
||||
}
|
||||
return NiFiCommon.LEAD_TRAIL_WHITE_SPACE_REGEX.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads the specified value to the specified width with the specified character.
|
||||
* If the specified value is already wider than the specified width, the original
|
||||
* value is returned.
|
||||
*
|
||||
* @param {integer} value
|
||||
* @param {integer} width
|
||||
* @param {string} character
|
||||
* @returns {string}
|
||||
*/
|
||||
pad(value: number, width: number, character: string): string {
|
||||
let s: string = value + '';
|
||||
|
||||
// pad until wide enough
|
||||
while (s.length < width) {
|
||||
s = character + s;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the specified DateTime.
|
||||
*
|
||||
* @param {Date} date
|
||||
* @returns {String}
|
||||
*/
|
||||
formatDateTime(date: Date): string {
|
||||
return (
|
||||
this.pad(date.getMonth() + 1, 2, '0') +
|
||||
'/' +
|
||||
this.pad(date.getDate(), 2, '0') +
|
||||
'/' +
|
||||
this.pad(date.getFullYear(), 2, '0') +
|
||||
' ' +
|
||||
this.pad(date.getHours(), 2, '0') +
|
||||
':' +
|
||||
this.pad(date.getMinutes(), 2, '0') +
|
||||
':' +
|
||||
this.pad(date.getSeconds(), 2, '0') +
|
||||
'.' +
|
||||
this.pad(date.getMilliseconds(), 3, '0')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the specified date time into a Date object. The resulting
|
||||
* object does not account for timezone and should only be used for
|
||||
* performing relative comparisons.
|
||||
*
|
||||
* @param {string} rawDateTime
|
||||
* @returns {Date}
|
||||
*/
|
||||
parseDateTime(rawDateTime: string): Date {
|
||||
// handle non date values
|
||||
if (!rawDateTime) {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
// parse the date time
|
||||
const dateTime: string[] = rawDateTime.split(/ /);
|
||||
|
||||
// ensure the correct number of tokens
|
||||
if (dateTime.length !== 3) {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
// get the date and time
|
||||
const date: string[] = dateTime[0].split(/\//);
|
||||
const time: string[] = dateTime[1].split(/:/);
|
||||
|
||||
// ensure the correct number of tokens
|
||||
if (date.length !== 3 || time.length !== 3) {
|
||||
return new Date();
|
||||
}
|
||||
const year: number = parseInt(date[2], 10);
|
||||
const month: number = parseInt(date[0], 10) - 1; // new Date() accepts months 0 - 11
|
||||
const day: number = parseInt(date[1], 10);
|
||||
const hours: number = parseInt(time[0], 10);
|
||||
const minutes: number = parseInt(time[1], 10);
|
||||
|
||||
// detect if there is millis
|
||||
const secondsSpec: string[] = time[2].split(/\./);
|
||||
const seconds: number = parseInt(secondsSpec[0], 10);
|
||||
let milliseconds: number = 0;
|
||||
if (secondsSpec.length === 2) {
|
||||
milliseconds = parseInt(secondsSpec[1], 10);
|
||||
}
|
||||
return new Date(year, month, day, hours, minutes, seconds, milliseconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the specified duration.
|
||||
*
|
||||
* @param {number} millis in millis
|
||||
*/
|
||||
formatDuration(millis: number): string {
|
||||
// don't support sub millisecond resolution
|
||||
let duration: number = millis < 1 ? 0 : millis;
|
||||
|
||||
// determine the number of days in the specified duration
|
||||
let days: number = duration / NiFiCommon.MILLIS_PER_DAY;
|
||||
days = days >= 1 ? Math.trunc(days) : 0;
|
||||
duration %= NiFiCommon.MILLIS_PER_DAY;
|
||||
|
||||
// remaining duration should be less than 1 day, get number of hours
|
||||
let hours: number = duration / NiFiCommon.MILLIS_PER_HOUR;
|
||||
hours = hours >= 1 ? Math.trunc(hours) : 0;
|
||||
duration %= NiFiCommon.MILLIS_PER_HOUR;
|
||||
|
||||
// remaining duration should be less than 1 hour, get number of minutes
|
||||
let minutes: number = duration / NiFiCommon.MILLIS_PER_MINUTE;
|
||||
minutes = minutes >= 1 ? Math.trunc(minutes) : 0;
|
||||
duration %= NiFiCommon.MILLIS_PER_MINUTE;
|
||||
|
||||
// remaining duration should be less than 1 minute, get number of seconds
|
||||
let seconds: number = duration / NiFiCommon.MILLIS_PER_SECOND;
|
||||
seconds = seconds >= 1 ? Math.trunc(seconds) : 0;
|
||||
|
||||
// remaining duration is the number millis (don't support sub millisecond resolution)
|
||||
duration = Math.floor(duration % NiFiCommon.MILLIS_PER_SECOND);
|
||||
|
||||
// format the time
|
||||
const time =
|
||||
this.pad(hours, 2, '0') +
|
||||
':' +
|
||||
this.pad(minutes, 2, '0') +
|
||||
':' +
|
||||
this.pad(seconds, 2, '0') +
|
||||
'.' +
|
||||
this.pad(duration, 3, '0');
|
||||
|
||||
// only include days if appropriate
|
||||
if (days > 0) {
|
||||
return days + ' days and ' + time;
|
||||
} else {
|
||||
return time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { LoadAboutResponse } from './index';
|
||||
|
||||
export const loadAbout = createAction('[About] Load About');
|
||||
|
||||
export const loadAboutSuccess = createAction('[About] Load About Success', props<{ response: LoadAboutResponse }>());
|
||||
|
||||
export const aboutApiError = createAction('[About] About Api Error', props<{ error: string }>());
|
||||
|
||||
export const clearAboutApiError = createAction('[User] Clear About Api Error');
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 AboutActions from './about.actions';
|
||||
import { catchError, from, map, of, switchMap } from 'rxjs';
|
||||
import { AboutService } from '../../service/about.service';
|
||||
|
||||
@Injectable()
|
||||
export class AboutEffects {
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private aboutService: AboutService
|
||||
) {}
|
||||
|
||||
loadAbout$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(AboutActions.loadAbout),
|
||||
switchMap(() => {
|
||||
return from(
|
||||
this.aboutService.getAbout().pipe(
|
||||
map((response) =>
|
||||
AboutActions.loadAboutSuccess({
|
||||
response: response
|
||||
})
|
||||
),
|
||||
catchError((error) => of(AboutActions.aboutApiError({ error: error.error })))
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 { AboutState } from './index';
|
||||
import { aboutApiError, clearAboutApiError, loadAbout, loadAboutSuccess } from './about.actions';
|
||||
|
||||
export const initialState: AboutState = {
|
||||
about: null,
|
||||
error: null,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
export const aboutReducer = createReducer(
|
||||
initialState,
|
||||
on(loadAbout, (state) => ({
|
||||
...state,
|
||||
status: 'loading' as const
|
||||
})),
|
||||
on(loadAboutSuccess, (state, { response }) => ({
|
||||
...state,
|
||||
about: response.about,
|
||||
error: null,
|
||||
status: 'success' as const
|
||||
})),
|
||||
on(aboutApiError, (state, { error }) => ({
|
||||
...state,
|
||||
error: error,
|
||||
status: 'error' as const
|
||||
})),
|
||||
on(clearAboutApiError, (state) => ({
|
||||
...state,
|
||||
error: null,
|
||||
status: 'pending' as const
|
||||
}))
|
||||
);
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { aboutFeatureKey, AboutState } from './index';
|
||||
|
||||
export const selectAboutState = createFeatureSelector<AboutState>(aboutFeatureKey);
|
||||
|
||||
export const selectAbout = createSelector(selectAboutState, (state: AboutState) => state.about);
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 aboutFeatureKey = 'about';
|
||||
|
||||
export interface LoadAboutResponse {
|
||||
about: About;
|
||||
}
|
||||
|
||||
export interface About {
|
||||
title: string;
|
||||
version: string;
|
||||
uri: string;
|
||||
contentViewerUrl: string;
|
||||
timezone: string;
|
||||
buildTag: string;
|
||||
buildRevision: string;
|
||||
buildBranch: string;
|
||||
buildTimestamp: string;
|
||||
}
|
||||
|
||||
export interface AboutState {
|
||||
about: About | null;
|
||||
error: string | null;
|
||||
status: 'pending' | 'loading' | 'error' | 'success';
|
||||
}
|
@ -21,15 +21,19 @@ import { UserState, userFeatureKey } from './user';
|
||||
import { userReducer } from './user/user.reducer';
|
||||
import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
|
||||
import { extensionTypesReducer } from './extension-types/extension-types.reducer';
|
||||
import { aboutFeatureKey, AboutState } from './about';
|
||||
import { aboutReducer } from './about/about.reducer';
|
||||
|
||||
export interface NiFiState {
|
||||
router: RouterReducerState;
|
||||
[userFeatureKey]: UserState;
|
||||
[extensionTypesFeatureKey]: ExtensionTypesState;
|
||||
[aboutFeatureKey]: AboutState;
|
||||
}
|
||||
|
||||
export const rootReducers: ActionReducerMap<NiFiState> = {
|
||||
router: routerReducer,
|
||||
[userFeatureKey]: userReducer,
|
||||
[extensionTypesFeatureKey]: extensionTypesReducer
|
||||
[extensionTypesFeatureKey]: extensionTypesReducer,
|
||||
[aboutFeatureKey]: aboutReducer
|
||||
};
|
||||
|
@ -20,6 +20,11 @@ export interface OkDialogRequest {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface CancelDialogRequest {
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface YesNoDialogRequest {
|
||||
title: string;
|
||||
message: string;
|
||||
@ -53,6 +58,65 @@ export interface EditControllerServiceDialogRequest {
|
||||
controllerService: ControllerServiceEntity;
|
||||
}
|
||||
|
||||
export interface ProvenanceEventSummary {
|
||||
id: string;
|
||||
eventId: number;
|
||||
eventTime: string;
|
||||
eventType: string;
|
||||
flowFileUuid: string;
|
||||
fileSize: string;
|
||||
fileSizeBytes: number;
|
||||
clusterNodeId?: string;
|
||||
clusterNodeAddress?: string;
|
||||
groupId: string;
|
||||
componentId: string;
|
||||
componentType: string;
|
||||
componentName: string;
|
||||
}
|
||||
|
||||
export interface Attribute {
|
||||
name: string;
|
||||
value: string;
|
||||
previousValue: string;
|
||||
}
|
||||
|
||||
export interface ProvenanceEvent extends ProvenanceEventSummary {
|
||||
eventDuration: string;
|
||||
lineageDuration: number;
|
||||
clusterNodeId: string;
|
||||
clusterNodeAddress: string;
|
||||
sourceSystemFlowFileId: string;
|
||||
alternateIdentifierUri: string;
|
||||
attributes: Attribute[];
|
||||
parentUuids: string[];
|
||||
childUuids: string[];
|
||||
transitUri: string;
|
||||
relationship: string;
|
||||
details: string;
|
||||
contentEqual: boolean;
|
||||
inputContentAvailable: boolean;
|
||||
inputContentClaimSection: string;
|
||||
inputContentClaimContainer: string;
|
||||
inputContentClaimIdentifier: string;
|
||||
inputContentClaimOffset: number;
|
||||
inputContentClaimFileSize: string;
|
||||
inputContentClaimFileSizeBytes: number;
|
||||
outputContentAvailable: boolean;
|
||||
outputContentClaimSection: string;
|
||||
outputContentClaimContainer: string;
|
||||
outputContentClaimIdentifier: string;
|
||||
outputContentClaimOffset: string;
|
||||
outputContentClaimFileSize: string;
|
||||
outputContentClaimFileSizeBytes: number;
|
||||
replayAvailable: boolean;
|
||||
replayExplanation: string;
|
||||
sourceConnectionIdentifier: string;
|
||||
}
|
||||
|
||||
export interface ProvenanceEventDialogRequest {
|
||||
event: ProvenanceEvent;
|
||||
}
|
||||
|
||||
export interface TextTipInput {
|
||||
text: string;
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title>{{ request.title }}</h2>
|
||||
<mat-dialog-content>
|
||||
<div class="text-sm">{{ request.message }}</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button type="button" (click)="cancelClicked()" mat-raised-button mat-dialog-close color="primary">Cancel</button>
|
||||
</mat-dialog-actions>
|
@ -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.
|
||||
*/
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { CancelDialog } from './cancel-dialog.component';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
|
||||
describe('CancelDialog', () => {
|
||||
let component: CancelDialog;
|
||||
let fixture: ComponentFixture<CancelDialog>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CancelDialog],
|
||||
providers: [
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {
|
||||
title: 'Title',
|
||||
message: 'Message'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
fixture = TestBed.createComponent(CancelDialog);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, EventEmitter, Inject, Output } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { CancelDialogRequest } from '../../../state/shared';
|
||||
|
||||
@Component({
|
||||
selector: 'cancel-dialog',
|
||||
standalone: true,
|
||||
imports: [MatDialogModule, MatButtonModule],
|
||||
templateUrl: './cancel-dialog.component.html',
|
||||
styleUrls: ['./cancel-dialog.component.scss']
|
||||
})
|
||||
export class CancelDialog {
|
||||
@Output() cancel: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
constructor(@Inject(MAT_DIALOG_DATA) public request: CancelDialogRequest) {}
|
||||
|
||||
cancelClicked(): void {
|
||||
this.cancel.next();
|
||||
}
|
||||
}
|
@ -260,8 +260,8 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
|
||||
type: property.descriptor.required
|
||||
? 'required'
|
||||
: property.descriptor.dynamic
|
||||
? 'userDefined'
|
||||
: 'optional'
|
||||
? 'userDefined'
|
||||
: 'optional'
|
||||
};
|
||||
|
||||
this.populateServiceLink(item);
|
||||
@ -307,8 +307,8 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
|
||||
type: property.descriptor.required
|
||||
? 'required'
|
||||
: property.descriptor.dynamic
|
||||
? 'userDefined'
|
||||
: 'optional'
|
||||
? 'userDefined'
|
||||
: 'optional'
|
||||
};
|
||||
|
||||
this.populateServiceLink(item);
|
||||
|
@ -0,0 +1,447 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title>Provenance Event</h2>
|
||||
<div class="provenance-event">
|
||||
<mat-dialog-content>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Details">
|
||||
<div class="tab-content py-4">
|
||||
<div class="absolute inset-0 flex gap-x-4">
|
||||
<div class="w-full flex flex-col gap-y-3">
|
||||
<div class="flex flex-col">
|
||||
<div>Time</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.eventTime }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>Event Duration</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatDuration;
|
||||
context: { $implicit: request.event.eventDuration }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>Lineage Duration</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatDuration;
|
||||
context: { $implicit: request.event.lineageDuration }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>Type</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.eventType }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>FlowFile UUID</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.flowFileUuid }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>File Size</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: {
|
||||
$implicit: request.event.fileSize,
|
||||
title: request.event.fileSizeBytes + ' bytes'
|
||||
}
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>Component Id</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.componentId }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>Component Name</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.componentName }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>Component Type</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.componentType }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>Details</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.details }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="request.event.eventType === 'RECEIVE'">
|
||||
<div class="flex flex-col">
|
||||
<div>Source FlowFile Id</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.sourceSystemFlowFileId }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div>Transit Uri</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.transitUri }
|
||||
"></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="request.event.eventType === 'SEND'">
|
||||
<div class="flex flex-col">
|
||||
<div>Transit Uri</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.transitUri }
|
||||
"></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="request.event.eventType === 'ADDINFO'">
|
||||
<div class="flex flex-col">
|
||||
<div>Alternate Identifier Uri</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.alternateIdentifierUri }
|
||||
"></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="request.event.eventType === 'ROUTE'">
|
||||
<div class="flex flex-col">
|
||||
<div>Relationship</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.relationship }
|
||||
"></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="request.event.eventType === 'FETCH'">
|
||||
<div class="flex flex-col" *ngIf="request.event.eventType === 'FETCH'">
|
||||
<div>Transit Uri</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.transitUri }
|
||||
"></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="request.event.clusterNodeId">
|
||||
<div class="flex flex-col">
|
||||
<div>Node Address</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: request.event.clusterNodeAddress }
|
||||
"></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #formatDuration let-duration>
|
||||
<ng-container *ngIf="duration != null; else noDuration">
|
||||
<div class="value">{{ formatDurationValue(duration) }}</div>
|
||||
</ng-container>
|
||||
<ng-template #noDuration>
|
||||
<div class="unset">No value set</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="w-full flex flex-col gap-y-3">
|
||||
<div class="flex flex-col">
|
||||
<div class="event-header">
|
||||
Parent FlowFiles ({{ request.event.parentUuids.length }})
|
||||
</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatUuids;
|
||||
context: { $implicit: request.event.parentUuids, emptyMessage: 'No parents' }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="event-header">Child FlowFiles ({{ request.event.childUuids.length }})</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatUuids;
|
||||
context: { $implicit: request.event.childUuids, emptyMessage: 'No children' }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<ng-template #formatUuids let-uuids let-emptyMessage="emptyMessage">
|
||||
<ng-container *ngIf="uuids.length > 0; else noUuids">
|
||||
<div class="value">
|
||||
<div *ngFor="let uuid of uuids">{{ uuid }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #noUuids>
|
||||
<div class="unset">{{ emptyMessage }}</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="Attributes">
|
||||
<div class="tab-content py-4">
|
||||
<div class="absolute inset-0 flex flex-col gap-y-4">
|
||||
<div class="flex justify-between">
|
||||
<div class="event-header">Attribute Values</div>
|
||||
<div class="flex items-center gap-x-1">
|
||||
<mat-checkbox [(ngModel)]="onlyShowModifiedAttributes"></mat-checkbox>
|
||||
<div>Show modified attributes only</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div *ngFor="let attribute of request.event.attributes">
|
||||
<div *ngIf="shouldShowAttribute(attribute)" class="mb-4 flex flex-col">
|
||||
<div>{{ attribute.name }}</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: attribute.value }
|
||||
"></ng-container>
|
||||
<ng-container *ngIf="attributeValueChanged(attribute)">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatValue;
|
||||
context: { $implicit: attribute.previousValue }
|
||||
"></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="Content">
|
||||
<div class="tab-content py-4">
|
||||
<div class="absolute inset-0 flex flex-col gap-y-4">
|
||||
<div class="flex gap-x-4">
|
||||
<div class="w-full">
|
||||
<div class="event-header">Input Claim</div>
|
||||
<div class="flex flex-col gap-y-3">
|
||||
<div>
|
||||
<div>Container</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: { $implicit: request.event.inputContentClaimContainer }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<div>Section</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: { $implicit: request.event.inputContentClaimSection }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<div>Identifier</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: { $implicit: request.event.inputContentClaimIdentifier }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<div>Offset</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: { $implicit: request.event.inputContentClaimOffset }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<div>Size</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: {
|
||||
$implicit: request.event.inputContentClaimFileSize,
|
||||
title: request.event.inputContentClaimFileSizeBytes + ' bytes'
|
||||
}
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div *ngIf="request.event.inputContentAvailable" class="flex">
|
||||
<button
|
||||
color="accent"
|
||||
mat-raised-button
|
||||
(click)="downloadContentClicked('input')">
|
||||
<i class="fa fa-download"></i>
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
*ngIf="contentViewerAvailable"
|
||||
class="ml-3"
|
||||
color="accent"
|
||||
mat-raised-button
|
||||
(click)="viewContentClicked('input')">
|
||||
<i class="fa fa-eye"></i>
|
||||
View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="event-header">Output Claim</div>
|
||||
<div class="flex flex-col gap-y-3">
|
||||
<div>
|
||||
<div>Container</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: { $implicit: request.event.outputContentClaimContainer }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<div>Section</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: { $implicit: request.event.outputContentClaimSection }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<div>Identifier</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: { $implicit: request.event.outputContentClaimIdentifier }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<div>Offset</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: { $implicit: request.event.outputContentClaimOffset }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<div>Size</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: {
|
||||
$implicit: request.event.outputContentClaimFileSize,
|
||||
title: request.event.outputContentClaimFileSizeBytes + ' bytes'
|
||||
}
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div *ngIf="request.event.outputContentAvailable" class="flex">
|
||||
<button
|
||||
color="accent"
|
||||
mat-raised-button
|
||||
(click)="downloadContentClicked('output')">
|
||||
<i class="fa fa-download"></i>
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
*ngIf="contentViewerAvailable"
|
||||
class="ml-3"
|
||||
color="accent"
|
||||
mat-raised-button
|
||||
(click)="viewContentClicked('output')">
|
||||
<i class="fa fa-eye"></i>
|
||||
View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="event-header">Replay</div>
|
||||
<div
|
||||
*ngIf="request.event.replayAvailable; else replayNotAvailable"
|
||||
class="flex flex-col gap-y-3">
|
||||
<div class="flex flex-col">
|
||||
<div>Connection Id</div>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
formatContentValue;
|
||||
context: { $implicit: request.event.sourceConnectionIdentifier }
|
||||
"></ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<button color="accent" mat-raised-button mat-dialog-close (click)="replayClicked()">
|
||||
<i class="fa fa-repeat"></i>
|
||||
Replay
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #replayNotAvailable>
|
||||
<div>{{ request.event.replayExplanation }}</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<ng-template #formatValue let-value let-title="title">
|
||||
<ng-container *ngIf="value != null; else nullValue">
|
||||
<ng-container *ngIf="value === ''; else nonEmptyValue">
|
||||
<div class="unset">Empty string set</div>
|
||||
</ng-container>
|
||||
<ng-template #nonEmptyValue>
|
||||
<div class="value" *ngIf="title == null; else valueWithTitle">{{ value }}</div>
|
||||
<ng-template #valueWithTitle>
|
||||
<div class="value" [title]="title">{{ value }}</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-template #nullValue>
|
||||
<div class="unset">No value set</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template #formatContentValue let-value let-title="title">
|
||||
<ng-container *ngIf="value != null; else nullValue">
|
||||
<div class="value" *ngIf="title == null; else valueWithTitle">{{ value }}</div>
|
||||
<ng-template #valueWithTitle>
|
||||
<div class="value" [title]="title">{{ value }}</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-template #nullValue>
|
||||
<div class="unset">No value previously set</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button color="primary" mat-raised-button mat-dialog-close>Ok</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
.provenance-event {
|
||||
@include mat.button-density(-1);
|
||||
|
||||
.mdc-dialog__content {
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
|
||||
.tab-content {
|
||||
position: relative;
|
||||
height: 475px;
|
||||
overflow-y: auto;
|
||||
|
||||
.event-header {
|
||||
color: #728e9b;
|
||||
font-size: 15px;
|
||||
font-family: 'Roboto Slab';
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mdc-text-field__input::-webkit-calendar-picker-indicator {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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 { ProvenanceEventDialog } from './provenance-event-dialog.component';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('ProvenanceEventDialog', () => {
|
||||
let component: ProvenanceEventDialog;
|
||||
let fixture: ComponentFixture<ProvenanceEventDialog>;
|
||||
|
||||
const data: any = {
|
||||
event: {
|
||||
id: '67231',
|
||||
eventId: 67231,
|
||||
eventTime: '12/06/2023 13:24:14.934 EST',
|
||||
lineageDuration: 80729691,
|
||||
eventType: 'DROP',
|
||||
flowFileUuid: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc',
|
||||
fileSize: '0 bytes',
|
||||
fileSizeBytes: 0,
|
||||
groupId: '36d207b9-018c-1000-6f5a-1b02d4517a78',
|
||||
componentId: '36d293df-018c-1000-b438-9e0cd664f5aa',
|
||||
componentType: 'Connection',
|
||||
componentName: 'success',
|
||||
attributes: [
|
||||
{
|
||||
name: 'filename',
|
||||
value: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc',
|
||||
previousValue: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc'
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
value: './',
|
||||
previousValue: './'
|
||||
},
|
||||
{
|
||||
name: 'uuid',
|
||||
value: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc',
|
||||
previousValue: '6908fd9d-9168-4da5-a92b-0414cdb5e3bc'
|
||||
}
|
||||
],
|
||||
parentUuids: [],
|
||||
childUuids: [],
|
||||
details: 'FlowFile Queue emptied by admin',
|
||||
contentEqual: false,
|
||||
inputContentAvailable: false,
|
||||
outputContentAvailable: false,
|
||||
outputContentClaimFileSize: '0 bytes',
|
||||
outputContentClaimFileSizeBytes: 0,
|
||||
replayAvailable: true,
|
||||
sourceConnectionIdentifier: '36d293df-018c-1000-b438-9e0cd664f5aa'
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ProvenanceEventDialog, BrowserAnimationsModule],
|
||||
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
|
||||
});
|
||||
fixture = TestBed.createComponent(ProvenanceEventDialog);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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, EventEmitter, Inject, Input, Output } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { NiFiCommon } from '../../../service/nifi-common.service';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { Attribute, ProvenanceEvent, ProvenanceEventDialogRequest } from '../../../state/shared';
|
||||
|
||||
@Component({
|
||||
selector: 'provenance-event-dialog',
|
||||
standalone: true,
|
||||
templateUrl: './provenance-event-dialog.component.html',
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatDialogModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
NgForOf,
|
||||
MatDatepickerModule,
|
||||
MatTabsModule,
|
||||
NgTemplateOutlet,
|
||||
FormsModule
|
||||
],
|
||||
styleUrls: ['./provenance-event-dialog.component.scss']
|
||||
})
|
||||
export class ProvenanceEventDialog {
|
||||
@Input() contentViewerAvailable!: boolean;
|
||||
|
||||
@Output() downloadContent: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Output() viewContent: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Output() replay: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
onlyShowModifiedAttributes: boolean = false;
|
||||
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public request: ProvenanceEventDialogRequest,
|
||||
private nifiCommon: NiFiCommon
|
||||
) {}
|
||||
|
||||
formatDurationValue(duration: number): string {
|
||||
if (duration === 0) {
|
||||
return '< 1 sec';
|
||||
}
|
||||
|
||||
return this.nifiCommon.formatDuration(duration);
|
||||
}
|
||||
|
||||
attributeValueChanged(attribute: Attribute): boolean {
|
||||
return attribute.value != attribute.previousValue;
|
||||
}
|
||||
|
||||
shouldShowAttribute(attribute: Attribute): boolean {
|
||||
// if the attribute value has changed, show it
|
||||
if (this.attributeValueChanged(attribute)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// attribute value hasn't changed, only show when
|
||||
// the user does not want to only see modified attributes
|
||||
return !this.onlyShowModifiedAttributes;
|
||||
}
|
||||
|
||||
downloadContentClicked(direction: string): void {
|
||||
this.downloadContent.next(direction);
|
||||
}
|
||||
|
||||
viewContentClicked(direction: string): void {
|
||||
this.viewContent.next(direction);
|
||||
}
|
||||
|
||||
replayClicked(): void {
|
||||
this.replay.next();
|
||||
}
|
||||
}
|
@ -20,11 +20,11 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
// Plus imports for other components in your app.
|
||||
@import '~roboto-fontface/css/roboto/roboto-fontface.css';
|
||||
@import 'assets/fonts/flowfont/flowfont.css';
|
||||
@import '~font-awesome/css/font-awesome.min.css';
|
||||
@import '~codemirror/lib/codemirror.css';
|
||||
@import '~codemirror/addon/hint/show-hint.css';
|
||||
@use 'roboto-fontface/css/roboto/roboto-fontface.css';
|
||||
@use 'assets/fonts/flowfont/flowfont.css';
|
||||
@use 'font-awesome/css/font-awesome.min.css';
|
||||
@use 'codemirror/lib/codemirror.css';
|
||||
@use 'codemirror/addon/hint/show-hint.css';
|
||||
|
||||
$fontPrimary: 'Roboto', sans-serif;
|
||||
$fontSecondary: 'Robot Slab', sans-serif;
|
||||
|
Loading…
x
Reference in New Issue
Block a user