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: {
|
headers: {
|
||||||
'X-ProxyPort': 4200
|
'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],
|
canMatch: [authGuard],
|
||||||
loadChildren: () => import('./pages/settings/feature/settings.module').then((m) => m.SettingsModule)
|
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',
|
path: 'parameter-contexts',
|
||||||
canMatch: [authGuard],
|
canMatch: [authGuard],
|
||||||
|
@ -20,7 +20,6 @@ import { BrowserModule } from '@angular/platform-browser';
|
|||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { FlowDesignerModule } from './pages/flow-designer/feature/flow-designer.module';
|
|
||||||
import { StoreModule } from '@ngrx/store';
|
import { StoreModule } from '@ngrx/store';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
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 { NavigationActionTiming, RouterState, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
import { rootReducers } from './state';
|
import { rootReducers } from './state';
|
||||||
import { UserEffects } from './state/user/user.effects';
|
import { UserEffects } from './state/user/user.effects';
|
||||||
import { LoginModule } from './pages/login/feature/login.module';
|
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { LoadingInterceptor } from './service/interceptors/loading.interceptor';
|
import { LoadingInterceptor } from './service/interceptors/loading.interceptor';
|
||||||
import { AuthInterceptor } from './service/interceptors/auth.interceptor';
|
import { AuthInterceptor } from './service/interceptors/auth.interceptor';
|
||||||
import { ExtensionTypesEffects } from './state/extension-types/extension-types.effects';
|
import { ExtensionTypesEffects } from './state/extension-types/extension-types.effects';
|
||||||
import { PollingInterceptor } from './service/interceptors/polling.interceptor';
|
import { PollingInterceptor } from './service/interceptors/polling.interceptor';
|
||||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
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
|
// @ts-ignore
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -54,13 +54,14 @@ import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
|||||||
routerState: RouterState.Minimal,
|
routerState: RouterState.Minimal,
|
||||||
navigationActionTiming: NavigationActionTiming.PostActivation
|
navigationActionTiming: NavigationActionTiming.PostActivation
|
||||||
}),
|
}),
|
||||||
EffectsModule.forRoot(UserEffects, ExtensionTypesEffects),
|
EffectsModule.forRoot(UserEffects, ExtensionTypesEffects, AboutEffects),
|
||||||
StoreDevtoolsModule.instrument({
|
StoreDevtoolsModule.instrument({
|
||||||
maxAge: 25,
|
maxAge: 25,
|
||||||
logOnly: environment.production,
|
logOnly: environment.production,
|
||||||
autoPause: true
|
autoPause: true
|
||||||
}),
|
}),
|
||||||
MatProgressSpinnerModule
|
MatProgressSpinnerModule,
|
||||||
|
MatNativeDateModule
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@ -27,6 +27,8 @@ import { CanvasState } from '../../state';
|
|||||||
import { transformFeatureKey } from '../../state/transform';
|
import { transformFeatureKey } from '../../state/transform';
|
||||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
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', () => {
|
describe('ConnectableBehavior', () => {
|
||||||
let service: ConnectableBehavior;
|
let service: ConnectableBehavior;
|
||||||
@ -46,6 +48,10 @@ describe('ConnectableBehavior', () => {
|
|||||||
{
|
{
|
||||||
selector: selectFlowState,
|
selector: selectFlowState,
|
||||||
value: initialState[flowFeatureKey]
|
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 { selectFlowState } from '../../state/flow/flow.selectors';
|
||||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
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', () => {
|
describe('DraggableBehavior', () => {
|
||||||
let service: DraggableBehavior;
|
let service: DraggableBehavior;
|
||||||
@ -51,6 +53,10 @@ describe('DraggableBehavior', () => {
|
|||||||
{
|
{
|
||||||
selector: selectTransform,
|
selector: selectTransform,
|
||||||
value: initialState[transformFeatureKey]
|
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 * as fromTransform from '../../state/transform/transform.reducer';
|
||||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
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', () => {
|
describe('QuickSelectBehavior', () => {
|
||||||
let service: QuickSelectBehavior;
|
let service: QuickSelectBehavior;
|
||||||
@ -46,6 +48,10 @@ describe('QuickSelectBehavior', () => {
|
|||||||
{
|
{
|
||||||
selector: selectFlowState,
|
selector: selectFlowState,
|
||||||
value: initialState[flowFeatureKey]
|
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 { selectTransform } from '../state/transform/transform.selectors';
|
||||||
import { controllerServicesFeatureKey } from '../state/controller-services';
|
import { controllerServicesFeatureKey } from '../state/controller-services';
|
||||||
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
|
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', () => {
|
describe('BirdseyeView', () => {
|
||||||
let service: BirdseyeView;
|
let service: BirdseyeView;
|
||||||
@ -51,6 +53,10 @@ describe('BirdseyeView', () => {
|
|||||||
{
|
{
|
||||||
selector: selectTransform,
|
selector: selectTransform,
|
||||||
value: initialState[transformFeatureKey]
|
value: initialState[transformFeatureKey]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: selectUser,
|
||||||
|
value: fromUser.initialState.user
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -26,12 +26,15 @@ import {
|
|||||||
selectCurrentProcessGroupId,
|
selectCurrentProcessGroupId,
|
||||||
selectParentProcessGroupId
|
selectParentProcessGroupId
|
||||||
} from '../state/flow/flow.selectors';
|
} 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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { BulletinsTip } from '../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
|
import { BulletinsTip } from '../../../ui/common/tooltips/bulletins-tip/bulletins-tip.component';
|
||||||
import { Position } from '../state/shared';
|
import { Position } from '../state/shared';
|
||||||
import { ComponentType, Permissions } from '../../../state/shared';
|
import { ComponentType, Permissions } from '../../../state/shared';
|
||||||
import { NiFiCommon } from '../../../service/nifi-common.service';
|
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({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -42,9 +45,10 @@ export class CanvasUtils {
|
|||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
private trimLengthCaches: Map<string, Map<string, Map<number, number>>> = new Map();
|
private trimLengthCaches: Map<string, Map<string, Map<number, number>>> = new Map();
|
||||||
private currentProcessGroupId: string = initialState.id;
|
private currentProcessGroupId: string = initialFlowState.id;
|
||||||
private parentProcessGroupId: string | null = initialState.flow.processGroupFlow.parentGroupId;
|
private parentProcessGroupId: string | null = initialFlowState.flow.processGroupFlow.parentGroupId;
|
||||||
private canvasPermissions: Permissions = initialState.flow.permissions;
|
private canvasPermissions: Permissions = initialFlowState.flow.permissions;
|
||||||
|
private currentUser: User = initialUserState.user;
|
||||||
private connections: any[] = [];
|
private connections: any[] = [];
|
||||||
|
|
||||||
private readonly humanizeDuration: Humanizer;
|
private readonly humanizeDuration: Humanizer;
|
||||||
@ -83,6 +87,13 @@ export class CanvasUtils {
|
|||||||
.subscribe((connections) => {
|
.subscribe((connections) => {
|
||||||
this.connections = connections;
|
this.connections = connections;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.store
|
||||||
|
.select(selectUser)
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.currentUser = user;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasDownstream(selection: any): boolean {
|
public hasDownstream(selection: any): boolean {
|
||||||
@ -124,6 +135,13 @@ export class CanvasUtils {
|
|||||||
d3.select('path.connector').remove();
|
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.
|
* Determines whether the specified selection is empty.
|
||||||
*
|
*
|
||||||
@ -435,6 +453,40 @@ export class CanvasUtils {
|
|||||||
return selection.size() === 1 && selection.classed('funnel');
|
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.
|
* 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 { selectTransform } from '../state/transform/transform.selectors';
|
||||||
import { controllerServicesFeatureKey } from '../state/controller-services';
|
import { controllerServicesFeatureKey } from '../state/controller-services';
|
||||||
import * as fromControllerServices from '../state/controller-services/controller-services.reducer';
|
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', () => {
|
describe('CanvasView', () => {
|
||||||
let service: CanvasView;
|
let service: CanvasView;
|
||||||
@ -51,6 +53,10 @@ describe('CanvasView', () => {
|
|||||||
{
|
{
|
||||||
selector: selectTransform,
|
selector: selectTransform,
|
||||||
value: initialState[transformFeatureKey]
|
value: initialState[transformFeatureKey]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: selectUser,
|
||||||
|
value: fromUser.initialState.user
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
CreateProcessGroupRequest,
|
CreateProcessGroupRequest,
|
||||||
CreateProcessorRequest,
|
CreateProcessorRequest,
|
||||||
DeleteComponentRequest,
|
DeleteComponentRequest,
|
||||||
|
ReplayLastProvenanceEventRequest,
|
||||||
Snippet,
|
Snippet,
|
||||||
UpdateComponentRequest,
|
UpdateComponentRequest,
|
||||||
UploadProcessGroupRequest
|
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> {
|
getProcessor(id: string): Observable<any> {
|
||||||
return this.httpClient.get(`${FlowService.API}/processors/${id}`);
|
return this.httpClient.get(`${FlowService.API}/processors/${id}`);
|
||||||
}
|
}
|
||||||
@ -245,4 +242,8 @@ export class FlowService {
|
|||||||
deleteSnippet(snippetId: string): Observable<any> {
|
deleteSnippet(snippetId: string): Observable<any> {
|
||||||
return this.httpClient.delete(`${FlowService.API}/snippets/${snippetId}`);
|
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 { selectTransform } from '../../state/transform/transform.selectors';
|
||||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
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', () => {
|
describe('ConnectionManager', () => {
|
||||||
let service: ConnectionManager;
|
let service: ConnectionManager;
|
||||||
@ -51,6 +53,10 @@ describe('ConnectionManager', () => {
|
|||||||
{
|
{
|
||||||
selector: selectTransform,
|
selector: selectTransform,
|
||||||
value: initialState[transformFeatureKey]
|
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 { selectTransform } from '../../state/transform/transform.selectors';
|
||||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
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', () => {
|
describe('FunnelManager', () => {
|
||||||
let service: FunnelManager;
|
let service: FunnelManager;
|
||||||
@ -51,6 +53,10 @@ describe('FunnelManager', () => {
|
|||||||
{
|
{
|
||||||
selector: selectTransform,
|
selector: selectTransform,
|
||||||
value: initialState[transformFeatureKey]
|
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 { selectTransform } from '../../state/transform/transform.selectors';
|
||||||
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
import { controllerServicesFeatureKey } from '../../state/controller-services';
|
||||||
import * as fromControllerServices from '../../state/controller-services/controller-services.reducer';
|
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', () => {
|
describe('RemoteProcessGroupManager', () => {
|
||||||
let service: RemoteProcessGroupManager;
|
let service: RemoteProcessGroupManager;
|
||||||
@ -51,6 +53,10 @@ describe('RemoteProcessGroupManager', () => {
|
|||||||
{
|
{
|
||||||
selector: selectTransform,
|
selector: selectTransform,
|
||||||
value: initialState[transformFeatureKey]
|
value: initialState[transformFeatureKey]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: selectUser,
|
||||||
|
value: fromUser.initialState.user
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -53,7 +53,8 @@ import {
|
|||||||
UpdatePositionsRequest,
|
UpdatePositionsRequest,
|
||||||
UploadProcessGroupRequest,
|
UploadProcessGroupRequest,
|
||||||
EditCurrentProcessGroupRequest,
|
EditCurrentProcessGroupRequest,
|
||||||
NavigateToControllerServicesRequest
|
NavigateToControllerServicesRequest,
|
||||||
|
ReplayLastProvenanceEventRequest
|
||||||
} from './index';
|
} from './index';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -375,3 +376,13 @@ export const renderConnectionsForComponent = createAction(
|
|||||||
'[Canvas] Render Connections For Component',
|
'[Canvas] Render Connections For Component',
|
||||||
props<{ id: string; updatePath: boolean; updateLabel: boolean }>()
|
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 }
|
{ 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(
|
showOkDialog$ = createEffect(
|
||||||
() =>
|
() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
|
@ -304,6 +304,11 @@ export interface NavigateToComponentRequest {
|
|||||||
processGroupId?: string;
|
processGroupId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReplayLastProvenanceEventRequest {
|
||||||
|
componentId: string;
|
||||||
|
nodes: string;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Snippets
|
Snippets
|
||||||
*/
|
*/
|
||||||
|
@ -30,7 +30,9 @@ import {
|
|||||||
navigateToControllerServicesForProcessGroup,
|
navigateToControllerServicesForProcessGroup,
|
||||||
navigateToEditComponent,
|
navigateToEditComponent,
|
||||||
navigateToEditCurrentProcessGroup,
|
navigateToEditCurrentProcessGroup,
|
||||||
reloadFlow
|
navigateToProvenanceForComponent,
|
||||||
|
reloadFlow,
|
||||||
|
replayLastProvenanceEvent
|
||||||
} from '../../../state/flow/flow.actions';
|
} from '../../../state/flow/flow.actions';
|
||||||
import { CanvasUtils } from '../../../service/canvas-utils.service';
|
import { CanvasUtils } from '../../../service/canvas-utils.service';
|
||||||
import { DeleteComponentRequest, MoveComponentRequest } from '../../../state/flow';
|
import { DeleteComponentRequest, MoveComponentRequest } from '../../../state/flow';
|
||||||
@ -154,24 +156,38 @@ export class ContextMenu implements OnInit {
|
|||||||
menuItems: [
|
menuItems: [
|
||||||
{
|
{
|
||||||
condition: function (canvasUtils: CanvasUtils, selection: any) {
|
condition: function (canvasUtils: CanvasUtils, selection: any) {
|
||||||
// TODO - canReplayProvenance
|
return canvasUtils.canReplayComponentProvenance(selection);
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
clazz: 'fa',
|
clazz: 'fa',
|
||||||
text: 'All nodes',
|
text: 'All nodes',
|
||||||
action: function (store: Store<CanvasState>) {
|
action: function (store: Store<CanvasState>, selection: any) {
|
||||||
// TODO - replayLastAllNodes
|
const selectionData = selection.datum();
|
||||||
|
store.dispatch(
|
||||||
|
replayLastProvenanceEvent({
|
||||||
|
request: {
|
||||||
|
componentId: selectionData.id,
|
||||||
|
nodes: 'ALL'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
condition: function (canvasUtils: CanvasUtils, selection: any) {
|
condition: function (canvasUtils: CanvasUtils, selection: any) {
|
||||||
// TODO - canReplayProvenance
|
return canvasUtils.canReplayComponentProvenance(selection);
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
clazz: 'fa',
|
clazz: 'fa',
|
||||||
text: 'Primary node',
|
text: 'Primary node',
|
||||||
action: function (store: Store<CanvasState>) {
|
action: function (store: Store<CanvasState>, selection: any) {
|
||||||
// TODO - replayLastPrimaryNode
|
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) {
|
condition: function (canvasUtils: CanvasUtils, selection: any) {
|
||||||
// TODO - canAccessProvenance
|
return canvasUtils.canAccessComponentProvenance(selection);
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
clazz: 'icon icon-provenance',
|
clazz: 'icon icon-provenance',
|
||||||
// imgStyle: 'context-menu-provenance',
|
// imgStyle: 'context-menu-provenance',
|
||||||
text: 'View data provenance',
|
text: 'View data provenance',
|
||||||
action: function (store: Store<CanvasState>) {
|
action: function (store: Store<CanvasState>, selection: any) {
|
||||||
// TODO - openProvenance
|
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 = new Map<string, ContextMenuDefinition>();
|
||||||
this.allMenus.set(this.ROOT_MENU.id, this.ROOT_MENU);
|
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.VERSION_MENU.id, this.VERSION_MENU);
|
||||||
this.allMenus.set(this.UPSTREAM_DOWNSTREAM.id, this.UPSTREAM_DOWNSTREAM);
|
this.allMenus.set(this.UPSTREAM_DOWNSTREAM.id, this.UPSTREAM_DOWNSTREAM);
|
||||||
this.allMenus.set(this.ALIGN.id, this.ALIGN);
|
this.allMenus.set(this.ALIGN.id, this.ALIGN);
|
||||||
|
@ -81,7 +81,11 @@
|
|||||||
Bulletin Board
|
Bulletin Board
|
||||||
</button>
|
</button>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<button mat-menu-item class="global-menu-item">
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
class="global-menu-item"
|
||||||
|
[routerLink]="['/provenance']"
|
||||||
|
[disabled]="!user.provenancePermissions.canRead">
|
||||||
<i class="icon fa-fw icon-provenance mr-2"></i>
|
<i class="icon fa-fw icon-provenance mr-2"></i>
|
||||||
Data Provenance
|
Data Provenance
|
||||||
</button>
|
</button>
|
||||||
|
@ -36,6 +36,8 @@ import { ClusterSummary, ControllerStatus } from '../../../state/flow';
|
|||||||
import { Search } from './search/search.component';
|
import { Search } from './search/search.component';
|
||||||
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { selectUser } from '../../../../../state/user/user.selectors';
|
||||||
|
import * as fromUser from '../../../../../state/user/user.reducer';
|
||||||
|
|
||||||
describe('HeaderComponent', () => {
|
describe('HeaderComponent', () => {
|
||||||
let component: HeaderComponent;
|
let component: HeaderComponent;
|
||||||
@ -99,6 +101,10 @@ describe('HeaderComponent', () => {
|
|||||||
{
|
{
|
||||||
selector: selectControllerBulletins,
|
selector: selectControllerBulletins,
|
||||||
value: []
|
value: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: selectUser,
|
||||||
|
value: fromUser.initialState.user
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@ -114,6 +114,7 @@ export class CreateProcessGroup {
|
|||||||
this.createProcessGroupForm
|
this.createProcessGroupForm
|
||||||
.get('newProcessGroupName')
|
.get('newProcessGroupName')
|
||||||
?.setValue(this.nifiCommon.substringBeforeLast(file.name, '.'));
|
?.setValue(this.nifiCommon.substringBeforeLast(file.name, '.'));
|
||||||
|
this.createProcessGroupForm.get('newProcessGroupName')?.markAsDirty();
|
||||||
this.createProcessGroupForm.get('newProcessGroupParameterContext')?.setValue(null);
|
this.createProcessGroupForm.get('newProcessGroupParameterContext')?.setValue(null);
|
||||||
this.flowNameAttached = file.name;
|
this.flowNameAttached = file.name;
|
||||||
this.flowDefinition = file;
|
this.flowDefinition = file;
|
||||||
|
@ -92,8 +92,4 @@ export class ParameterContextService {
|
|||||||
params: revision
|
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'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class NiFiCommon {
|
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() {}
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -171,4 +176,144 @@ export class NiFiCommon {
|
|||||||
}
|
}
|
||||||
return NiFiCommon.LEAD_TRAIL_WHITE_SPACE_REGEX.test(value);
|
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 { userReducer } from './user/user.reducer';
|
||||||
import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
|
import { extensionTypesFeatureKey, ExtensionTypesState } from './extension-types';
|
||||||
import { extensionTypesReducer } from './extension-types/extension-types.reducer';
|
import { extensionTypesReducer } from './extension-types/extension-types.reducer';
|
||||||
|
import { aboutFeatureKey, AboutState } from './about';
|
||||||
|
import { aboutReducer } from './about/about.reducer';
|
||||||
|
|
||||||
export interface NiFiState {
|
export interface NiFiState {
|
||||||
router: RouterReducerState;
|
router: RouterReducerState;
|
||||||
[userFeatureKey]: UserState;
|
[userFeatureKey]: UserState;
|
||||||
[extensionTypesFeatureKey]: ExtensionTypesState;
|
[extensionTypesFeatureKey]: ExtensionTypesState;
|
||||||
|
[aboutFeatureKey]: AboutState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rootReducers: ActionReducerMap<NiFiState> = {
|
export const rootReducers: ActionReducerMap<NiFiState> = {
|
||||||
router: routerReducer,
|
router: routerReducer,
|
||||||
[userFeatureKey]: userReducer,
|
[userFeatureKey]: userReducer,
|
||||||
[extensionTypesFeatureKey]: extensionTypesReducer
|
[extensionTypesFeatureKey]: extensionTypesReducer,
|
||||||
|
[aboutFeatureKey]: aboutReducer
|
||||||
};
|
};
|
||||||
|
@ -20,6 +20,11 @@ export interface OkDialogRequest {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CancelDialogRequest {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface YesNoDialogRequest {
|
export interface YesNoDialogRequest {
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
@ -53,6 +58,65 @@ export interface EditControllerServiceDialogRequest {
|
|||||||
controllerService: ControllerServiceEntity;
|
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 {
|
export interface TextTipInput {
|
||||||
text: string;
|
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
|
type: property.descriptor.required
|
||||||
? 'required'
|
? 'required'
|
||||||
: property.descriptor.dynamic
|
: property.descriptor.dynamic
|
||||||
? 'userDefined'
|
? 'userDefined'
|
||||||
: 'optional'
|
: 'optional'
|
||||||
};
|
};
|
||||||
|
|
||||||
this.populateServiceLink(item);
|
this.populateServiceLink(item);
|
||||||
@ -307,8 +307,8 @@ export class PropertyTable implements AfterViewInit, ControlValueAccessor {
|
|||||||
type: property.descriptor.required
|
type: property.descriptor.required
|
||||||
? 'required'
|
? 'required'
|
||||||
: property.descriptor.dynamic
|
: property.descriptor.dynamic
|
||||||
? 'userDefined'
|
? 'userDefined'
|
||||||
: 'optional'
|
: 'optional'
|
||||||
};
|
};
|
||||||
|
|
||||||
this.populateServiceLink(item);
|
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;
|
@use '@angular/material' as mat;
|
||||||
|
|
||||||
// Plus imports for other components in your app.
|
// Plus imports for other components in your app.
|
||||||
@import '~roboto-fontface/css/roboto/roboto-fontface.css';
|
@use 'roboto-fontface/css/roboto/roboto-fontface.css';
|
||||||
@import 'assets/fonts/flowfont/flowfont.css';
|
@use 'assets/fonts/flowfont/flowfont.css';
|
||||||
@import '~font-awesome/css/font-awesome.min.css';
|
@use 'font-awesome/css/font-awesome.min.css';
|
||||||
@import '~codemirror/lib/codemirror.css';
|
@use 'codemirror/lib/codemirror.css';
|
||||||
@import '~codemirror/addon/hint/show-hint.css';
|
@use 'codemirror/addon/hint/show-hint.css';
|
||||||
|
|
||||||
$fontPrimary: 'Roboto', sans-serif;
|
$fontPrimary: 'Roboto', sans-serif;
|
||||||
$fontSecondary: 'Robot Slab', sans-serif;
|
$fontSecondary: 'Robot Slab', sans-serif;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user