mirror of https://github.com/apache/nifi.git
[NIFI-12754] - Flow Configuration History (#8399)
* [NIFI-12754] - Flow Configuration History * support selection * support pagination * support sorting * added time controls, also updated provenance to use them too * allow for clearing of filters * use date range filter * more details dialog for flow config history * support purge history * review feedback - use snackbar error where intended, add padding between header and page content * don't use route for flow config history item selection * Address review feedback * remove unused style * Review feedback * initial query is for the last 2 weeks * added timezone to purge confirmation message * reset pagination state on filter, clear filter, and purge * only resubmit query when filter by changes IF there is a filter term specified This closes #8399
This commit is contained in:
parent
22de416ffc
commit
2792aa7038
|
@ -77,6 +77,14 @@ const routes: Routes = [
|
||||||
canMatch: [authenticationGuard],
|
canMatch: [authenticationGuard],
|
||||||
loadChildren: () => import('./pages/queue/feature/queue.module').then((m) => m.QueueModule)
|
loadChildren: () => import('./pages/queue/feature/queue.module').then((m) => m.QueueModule)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'flow-configuration-history',
|
||||||
|
canMatch: [authenticationGuard],
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/flow-configuration-history/feature/flow-configuration-history.module').then(
|
||||||
|
(m) => m.FlowConfigurationHistoryModule
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
canMatch: [authenticationGuard],
|
canMatch: [authenticationGuard],
|
||||||
|
|
|
@ -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 { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { FlowConfigurationHistory } from './flow-configuration-history.component';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: FlowConfigurationHistory
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class FlowConfigurationHistoryRoutingModule {}
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!--
|
||||||
|
~ Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
~ contributor license agreements. See the NOTICE file distributed with
|
||||||
|
~ this work for additional information regarding copyright ownership.
|
||||||
|
~ The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
~ (the "License"); you may not use this file except in compliance with
|
||||||
|
~ the License. You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="pb-5 flex flex-col h-screen justify-between gap-y-5">
|
||||||
|
<header class="nifi-header">
|
||||||
|
<navigation></navigation>
|
||||||
|
</header>
|
||||||
|
<div class="px-5 flex-1 flex flex-col">
|
||||||
|
<h3 class="text-xl bold counter-header">Flow Configuration History</h3>
|
||||||
|
<flow-configuration-history-listing class="flex-1"></flow-configuration-history-listing>
|
||||||
|
</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,53 @@
|
||||||
|
/*
|
||||||
|
* 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 { FlowConfigurationHistory } from './flow-configuration-history.component';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { initialHistoryState } from '../state/flow-configuration-history-listing/flow-configuration-history-listing.reducer';
|
||||||
|
import { FlowConfigurationHistoryListing } from '../ui/flow-configuration-history-listing/flow-configuration-history-listing.component';
|
||||||
|
|
||||||
|
describe('FlowConfigurationHistory', () => {
|
||||||
|
let component: FlowConfigurationHistory;
|
||||||
|
let fixture: ComponentFixture<FlowConfigurationHistory>;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'navigation',
|
||||||
|
standalone: true,
|
||||||
|
template: ''
|
||||||
|
})
|
||||||
|
class MockNavigation {}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [FlowConfigurationHistory],
|
||||||
|
imports: [RouterModule, RouterTestingModule, MockNavigation, FlowConfigurationHistoryListing],
|
||||||
|
providers: [provideMockStore({ initialState: initialHistoryState })]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(FlowConfigurationHistory);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'flow-configuration-history',
|
||||||
|
templateUrl: './flow-configuration-history.component.html',
|
||||||
|
styleUrls: ['./flow-configuration-history.component.scss']
|
||||||
|
})
|
||||||
|
export class FlowConfigurationHistory {}
|
|
@ -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 { FlowConfigurationHistory } from './flow-configuration-history.component';
|
||||||
|
import { Navigation } from '../../../ui/common/navigation/navigation.component';
|
||||||
|
import { FlowConfigurationHistoryRoutingModule } from './flow-configuration-history-routing.module';
|
||||||
|
import { FlowConfigurationHistoryListing } from '../ui/flow-configuration-history-listing/flow-configuration-history-listing.component';
|
||||||
|
import { StoreModule } from '@ngrx/store';
|
||||||
|
import { flowConfigurationHistoryFeatureKey, reducers } from '../state';
|
||||||
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
import { FlowConfigurationHistoryListingEffects } from '../state/flow-configuration-history-listing/flow-configuration-history-listing.effects';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
Navigation,
|
||||||
|
FlowConfigurationHistoryRoutingModule,
|
||||||
|
FlowConfigurationHistoryListing,
|
||||||
|
StoreModule.forFeature(flowConfigurationHistoryFeatureKey, reducers),
|
||||||
|
EffectsModule.forFeature(FlowConfigurationHistoryListingEffects)
|
||||||
|
],
|
||||||
|
declarations: [FlowConfigurationHistory],
|
||||||
|
exports: [FlowConfigurationHistory]
|
||||||
|
})
|
||||||
|
export class FlowConfigurationHistoryModule {}
|
|
@ -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 { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { HistoryQueryRequest, PurgeHistoryRequest } from '../state/flow-configuration-history-listing';
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class FlowConfigurationHistoryService {
|
||||||
|
private static readonly API: string = '../nifi-api';
|
||||||
|
|
||||||
|
constructor(private httpClient: HttpClient) {}
|
||||||
|
|
||||||
|
getHistory(request: HistoryQueryRequest): Observable<any> {
|
||||||
|
return this.httpClient.get(`${FlowConfigurationHistoryService.API}/flow/history`, { params: { ...request } });
|
||||||
|
}
|
||||||
|
|
||||||
|
purgeHistory(request: PurgeHistoryRequest): Observable<any> {
|
||||||
|
return this.httpClient.delete(`${FlowConfigurationHistoryService.API}/controller/history`, {
|
||||||
|
params: { ...request }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
|
ActionEntity,
|
||||||
|
HistoryEntity,
|
||||||
|
HistoryQueryRequest,
|
||||||
|
PurgeHistoryRequest,
|
||||||
|
SelectFlowConfigurationHistoryRequest
|
||||||
|
} from './index';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
|
||||||
|
const HISTORY_PREFIX = '[Flow Configuration History Listing]';
|
||||||
|
|
||||||
|
export const loadHistory = createAction(`${HISTORY_PREFIX} Load Listing`, props<{ request: HistoryQueryRequest }>());
|
||||||
|
|
||||||
|
export const loadHistorySuccess = createAction(
|
||||||
|
`${HISTORY_PREFIX} Load Listing Success`,
|
||||||
|
props<{ response: HistoryEntity }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const resetHistoryState = createAction(`${HISTORY_PREFIX} Reset History State`);
|
||||||
|
|
||||||
|
export const clearHistorySelection = createAction(`${HISTORY_PREFIX} Clear Selection`);
|
||||||
|
|
||||||
|
export const selectHistoryItem = createAction(
|
||||||
|
`${HISTORY_PREFIX} Select History Item`,
|
||||||
|
props<{ request: SelectFlowConfigurationHistoryRequest }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const flowConfigurationHistorySnackbarError = createAction(
|
||||||
|
`${HISTORY_PREFIX} Flow Configuration History Snackbar Error`,
|
||||||
|
props<{ errorResponse: HttpErrorResponse }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const openMoreDetailsDialog = createAction(
|
||||||
|
`${HISTORY_PREFIX} Open More Details Dialog`,
|
||||||
|
props<{ request: ActionEntity }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const openPurgeHistoryDialog = createAction(`${HISTORY_PREFIX} Open Purge History Dialog`);
|
||||||
|
|
||||||
|
export const purgeHistory = createAction(`${HISTORY_PREFIX} Purge History`, props<{ request: PurgeHistoryRequest }>());
|
||||||
|
|
||||||
|
export const purgeHistorySuccess = createAction(`${HISTORY_PREFIX} Purge History Success`);
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* 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, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { NiFiState } from '../../../../state';
|
||||||
|
import { ErrorHelper } from '../../../../service/error-helper.service';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import * as HistoryActions from './flow-configuration-history-listing.actions';
|
||||||
|
import { catchError, from, map, of, switchMap, take, tap } from 'rxjs';
|
||||||
|
import { selectHistoryQuery, selectHistoryStatus } from './flow-configuration-history-listing.selectors';
|
||||||
|
import { FlowConfigurationHistoryService } from '../../service/flow-configuration-history.service';
|
||||||
|
import { HistoryEntity } from './index';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ActionDetails } from '../../ui/flow-configuration-history-listing/action-details/action-details.component';
|
||||||
|
import { PurgeHistory } from '../../ui/flow-configuration-history-listing/purge-history/purge-history.component';
|
||||||
|
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
|
||||||
|
import { isDefinedAndNotNull } from '../../../../state/shared';
|
||||||
|
import * as ErrorActions from '../../../../state/error/error.actions';
|
||||||
|
import { selectAbout } from '../../../../state/about/about.selectors';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FlowConfigurationHistoryListingEffects {
|
||||||
|
constructor(
|
||||||
|
private actions$: Actions,
|
||||||
|
private store: Store<NiFiState>,
|
||||||
|
private errorHelper: ErrorHelper,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private historyService: FlowConfigurationHistoryService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
loadHistory$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(HistoryActions.loadHistory),
|
||||||
|
map((action) => action.request),
|
||||||
|
concatLatestFrom(() => this.store.select(selectHistoryStatus)),
|
||||||
|
switchMap(([request, status]) =>
|
||||||
|
from(this.historyService.getHistory(request)).pipe(
|
||||||
|
map((response: HistoryEntity) =>
|
||||||
|
HistoryActions.loadHistorySuccess({
|
||||||
|
response: response
|
||||||
|
})
|
||||||
|
),
|
||||||
|
catchError((errorResponse: HttpErrorResponse) =>
|
||||||
|
of(this.errorHelper.handleLoadingError(status, errorResponse))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
flowConfigurationHistorySnackbarError = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(HistoryActions.flowConfigurationHistorySnackbarError),
|
||||||
|
map((action) => action.errorResponse),
|
||||||
|
switchMap((errorResponse) => of(ErrorActions.snackBarError({ error: errorResponse.error })))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
openMoreDetailsDialog$ = createEffect(
|
||||||
|
() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(HistoryActions.openMoreDetailsDialog),
|
||||||
|
map((action) => action.request),
|
||||||
|
tap((actionEntity) => {
|
||||||
|
this.dialog.open(ActionDetails, {
|
||||||
|
data: actionEntity,
|
||||||
|
panelClass: 'medium-dialog'
|
||||||
|
});
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{ dispatch: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
openPurgeHistoryDialog$ = createEffect(
|
||||||
|
() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(HistoryActions.openPurgeHistoryDialog),
|
||||||
|
tap(() => {
|
||||||
|
const dialogReference = this.dialog.open(PurgeHistory, {
|
||||||
|
panelClass: 'medium-short-dialog'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogReference.componentInstance.submitPurgeRequest
|
||||||
|
.pipe(
|
||||||
|
isDefinedAndNotNull(),
|
||||||
|
concatLatestFrom(() => this.store.select(selectAbout).pipe(isDefinedAndNotNull())),
|
||||||
|
take(1)
|
||||||
|
)
|
||||||
|
.subscribe(([result, about]) => {
|
||||||
|
const yesNoRef = this.dialog.open(YesNoDialog, {
|
||||||
|
data: {
|
||||||
|
title: 'Confirm History Purge',
|
||||||
|
message: `Are you sure you want to delete all history before '${result.endDate} ${about.timezone}'?`
|
||||||
|
},
|
||||||
|
panelClass: 'small-dialog'
|
||||||
|
});
|
||||||
|
|
||||||
|
yesNoRef.componentInstance.yes.pipe(take(1)).subscribe(() => {
|
||||||
|
this.store.dispatch(HistoryActions.purgeHistory({ request: { ...result } }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{
|
||||||
|
dispatch: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
purgeHistory$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(HistoryActions.purgeHistory),
|
||||||
|
map((action) => action.request),
|
||||||
|
switchMap((request) =>
|
||||||
|
from(this.historyService.purgeHistory(request)).pipe(
|
||||||
|
map(() => HistoryActions.purgeHistorySuccess()),
|
||||||
|
catchError((errorResponse: HttpErrorResponse) =>
|
||||||
|
of(HistoryActions.flowConfigurationHistorySnackbarError({ errorResponse }))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
purgeHistorySuccess$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(HistoryActions.purgeHistorySuccess),
|
||||||
|
concatLatestFrom(() => this.store.select(selectHistoryQuery)),
|
||||||
|
switchMap(([, query]) => {
|
||||||
|
if (query) {
|
||||||
|
return of(HistoryActions.loadHistory({ request: { ...query } }));
|
||||||
|
}
|
||||||
|
return of(HistoryActions.loadHistory({ request: { count: 50, offset: 0 } }));
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 { FlowConfigurationHistoryListingState } from './index';
|
||||||
|
import { createReducer, on } from '@ngrx/store';
|
||||||
|
import {
|
||||||
|
clearHistorySelection,
|
||||||
|
flowConfigurationHistorySnackbarError,
|
||||||
|
loadHistory,
|
||||||
|
loadHistorySuccess,
|
||||||
|
purgeHistory,
|
||||||
|
purgeHistorySuccess,
|
||||||
|
resetHistoryState,
|
||||||
|
selectHistoryItem
|
||||||
|
} from './flow-configuration-history-listing.actions';
|
||||||
|
|
||||||
|
export const initialHistoryState: FlowConfigurationHistoryListingState = {
|
||||||
|
actions: [],
|
||||||
|
total: 0,
|
||||||
|
status: 'pending',
|
||||||
|
loadedTimestamp: '',
|
||||||
|
query: null,
|
||||||
|
selectedId: null,
|
||||||
|
purging: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export const flowConfigurationHistoryListingReducer = createReducer(
|
||||||
|
initialHistoryState,
|
||||||
|
|
||||||
|
on(loadHistory, (state, { request }) => ({
|
||||||
|
...state,
|
||||||
|
status: 'loading' as const,
|
||||||
|
query: request
|
||||||
|
})),
|
||||||
|
|
||||||
|
on(loadHistorySuccess, (state, { response }) => ({
|
||||||
|
...state,
|
||||||
|
status: 'success' as const,
|
||||||
|
loadedTimestamp: response.history.lastRefreshed,
|
||||||
|
actions: response.history.actions,
|
||||||
|
total: response.history.total
|
||||||
|
})),
|
||||||
|
|
||||||
|
on(resetHistoryState, () => ({
|
||||||
|
...initialHistoryState
|
||||||
|
})),
|
||||||
|
|
||||||
|
on(purgeHistory, (state) => ({
|
||||||
|
...state,
|
||||||
|
query: {
|
||||||
|
...state.query,
|
||||||
|
count: 50,
|
||||||
|
offset: 0
|
||||||
|
},
|
||||||
|
purging: true
|
||||||
|
})),
|
||||||
|
|
||||||
|
on(purgeHistorySuccess, flowConfigurationHistorySnackbarError, (state) => ({
|
||||||
|
...state,
|
||||||
|
purging: false
|
||||||
|
})),
|
||||||
|
|
||||||
|
on(selectHistoryItem, (state, { request }) => ({
|
||||||
|
...state,
|
||||||
|
selectedId: request.id
|
||||||
|
})),
|
||||||
|
|
||||||
|
on(clearHistorySelection, (state) => ({
|
||||||
|
...state,
|
||||||
|
selectedId: null
|
||||||
|
}))
|
||||||
|
);
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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 { FlowConfigurationHistoryState, selectFlowConfigurationHistoryState } from '../index';
|
||||||
|
import { flowConfigurationHistoryListingFeatureKey, FlowConfigurationHistoryListingState } from './index';
|
||||||
|
|
||||||
|
export const selectFlowConfigurationHistoryListingState = createSelector(
|
||||||
|
selectFlowConfigurationHistoryState,
|
||||||
|
(state: FlowConfigurationHistoryState) => state[flowConfigurationHistoryListingFeatureKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectHistoryActions = createSelector(
|
||||||
|
selectFlowConfigurationHistoryListingState,
|
||||||
|
(state: FlowConfigurationHistoryListingState) => state.actions
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectHistoryStatus = createSelector(
|
||||||
|
selectFlowConfigurationHistoryListingState,
|
||||||
|
(state: FlowConfigurationHistoryListingState) => state.status
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectHistoryLoadedTimestamp = createSelector(
|
||||||
|
selectFlowConfigurationHistoryListingState,
|
||||||
|
(state: FlowConfigurationHistoryListingState) => state.loadedTimestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectHistoryQuery = createSelector(
|
||||||
|
selectFlowConfigurationHistoryListingState,
|
||||||
|
(state: FlowConfigurationHistoryListingState) => state.query
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectHistoryTotalResults = createSelector(
|
||||||
|
selectFlowConfigurationHistoryListingState,
|
||||||
|
(state: FlowConfigurationHistoryListingState) => state.total
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectedHistoryItem = createSelector(
|
||||||
|
selectFlowConfigurationHistoryListingState,
|
||||||
|
(state: FlowConfigurationHistoryListingState) => state.selectedId
|
||||||
|
);
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* 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 flowConfigurationHistoryListingFeatureKey = 'listing';
|
||||||
|
|
||||||
|
// Returned from API call to /nifi-api/flow/history
|
||||||
|
export interface HistoryEntity {
|
||||||
|
history: History;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface History {
|
||||||
|
total: number;
|
||||||
|
lastRefreshed: string;
|
||||||
|
actions: ActionEntity[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionEntity {
|
||||||
|
id: number;
|
||||||
|
timestamp: string;
|
||||||
|
sourceId: string;
|
||||||
|
canRead: boolean;
|
||||||
|
action: Action;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Action {
|
||||||
|
id: number;
|
||||||
|
userIdentity: string;
|
||||||
|
timestamp: string;
|
||||||
|
sourceId: string;
|
||||||
|
sourceName: string;
|
||||||
|
sourceType: string;
|
||||||
|
componentDetails?: ExtensionDetails | RemoteProcessGroupDetails;
|
||||||
|
operation: string;
|
||||||
|
actionDetails?: ConfigureActionDetails | MoveActionDetails | ConnectionActionDetails | PurgeActionDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionDetails {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoteProcessGroupDetails {
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigureActionDetails {
|
||||||
|
name: string;
|
||||||
|
previousValue: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoveActionDetails {
|
||||||
|
previousGroupId: string;
|
||||||
|
previousGroup: string;
|
||||||
|
groupId: string;
|
||||||
|
group: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectionActionDetails {
|
||||||
|
sourceId: string;
|
||||||
|
sourceName: string;
|
||||||
|
sourceType: string;
|
||||||
|
relationship: string;
|
||||||
|
destinationId: string;
|
||||||
|
destinationName: string;
|
||||||
|
destinationType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurgeActionDetails {
|
||||||
|
endDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HistoryQueryRequest {
|
||||||
|
count: number;
|
||||||
|
offset: number;
|
||||||
|
sortColumn?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
startDate?: string; // MM/dd/yyyy HH:mm:ss
|
||||||
|
endDate?: string; // MM/dd/yyyy HH:mm:ss
|
||||||
|
userIdentity?: string;
|
||||||
|
sourceId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowConfigurationHistoryListingState {
|
||||||
|
actions: ActionEntity[];
|
||||||
|
total: number;
|
||||||
|
query: HistoryQueryRequest | null;
|
||||||
|
loadedTimestamp: string;
|
||||||
|
purging: boolean;
|
||||||
|
selectedId: number | null;
|
||||||
|
status: 'pending' | 'loading' | 'success';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectFlowConfigurationHistoryRequest {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurgeHistoryRequest {
|
||||||
|
endDate: string; // MM/dd/yyyy HH:mm:ss
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
flowConfigurationHistoryListingFeatureKey,
|
||||||
|
FlowConfigurationHistoryListingState
|
||||||
|
} from './flow-configuration-history-listing';
|
||||||
|
import { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
|
||||||
|
import { flowConfigurationHistoryListingReducer } from './flow-configuration-history-listing/flow-configuration-history-listing.reducer';
|
||||||
|
|
||||||
|
export const flowConfigurationHistoryFeatureKey = 'flowConfigurationHistory';
|
||||||
|
|
||||||
|
export interface FlowConfigurationHistoryState {
|
||||||
|
[flowConfigurationHistoryListingFeatureKey]: FlowConfigurationHistoryListingState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reducers(state: FlowConfigurationHistoryState | undefined, action: Action) {
|
||||||
|
return combineReducers({
|
||||||
|
[flowConfigurationHistoryListingFeatureKey]: flowConfigurationHistoryListingReducer
|
||||||
|
})(state, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectFlowConfigurationHistoryState = createFeatureSelector<FlowConfigurationHistoryState>(
|
||||||
|
flowConfigurationHistoryFeatureKey
|
||||||
|
);
|
|
@ -0,0 +1,213 @@
|
||||||
|
<!--
|
||||||
|
~ 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="action-details h-full">
|
||||||
|
<h2 mat-dialog-title>Action Details</h2>
|
||||||
|
<div class="action-details-content h-full">
|
||||||
|
<mat-dialog-content>
|
||||||
|
<div class="panel-content flex flex-col h-full w-full gap-y-4">
|
||||||
|
<div>
|
||||||
|
<div>Id</div>
|
||||||
|
<div class="value">{{ actionEntity.sourceId }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="actionEntity.action?.componentDetails">
|
||||||
|
<div *ngIf="isRemoteProcessGroup(actionEntity); else extension">
|
||||||
|
<div>Uri</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: getRemoteProcessGroupDetails(actionEntity)?.uri }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #extension>
|
||||||
|
<div>
|
||||||
|
<div>Type</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: getExtensionDetails(actionEntity)?.type }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="actionEntity.action?.actionDetails">
|
||||||
|
<ng-container [ngSwitch]="actionEntity.action.operation">
|
||||||
|
<ng-container *ngSwitchCase="'Configure'">
|
||||||
|
<ng-container *ngIf="getConfigureActionDetails(actionEntity) as details">
|
||||||
|
<div>
|
||||||
|
<div>Name</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.name }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Value</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.value }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Previous Value</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.previousValue }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'Connect' || 'Disconnect'">
|
||||||
|
<ng-container *ngIf="getConnectActionDetails(actionEntity) as details">
|
||||||
|
<div>
|
||||||
|
<div>Source Id</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.sourceId }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Source Name</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.sourceName }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Source Type</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.sourceType }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Relationship(s)</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.relationship }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Destination Id</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.destinationId }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Destination Name</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.destinationName }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Destination Type</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.destinationType }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'Move'">
|
||||||
|
<ng-container *ngIf="getMoveActionDetails(actionEntity) as details">
|
||||||
|
<div>
|
||||||
|
<div>Group</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.group }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Group Id</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.groupId }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Previous Group</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.previousGroup }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>Previous Group Id</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.previousGroupId }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'Purge'">
|
||||||
|
<ng-container *ngIf="getPurgeActionDetails(actionEntity) as details">
|
||||||
|
<div>
|
||||||
|
<div>End Date</div>
|
||||||
|
<ng-container
|
||||||
|
*ngTemplateOutlet="
|
||||||
|
formatValue;
|
||||||
|
context: { $implicit: details.endDate }
|
||||||
|
"></ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</mat-dialog-content>
|
||||||
|
<mat-dialog-actions align="end">
|
||||||
|
<button color="primary" mat-raised-button mat-dialog-close>Ok</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.action-details {
|
||||||
|
.action-details-content {
|
||||||
|
.mdc-dialog__content {
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
position: relative;
|
||||||
|
height: 350px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* 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 { ActionDetails } from './action-details.component';
|
||||||
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
import { ActionEntity } from '../../../state/flow-configuration-history-listing';
|
||||||
|
|
||||||
|
describe('ActionDetails', () => {
|
||||||
|
let component: ActionDetails;
|
||||||
|
let fixture: ComponentFixture<ActionDetails>;
|
||||||
|
const data: ActionEntity = {
|
||||||
|
id: 276,
|
||||||
|
timestamp: '02/12/2024 12:52:54 EST',
|
||||||
|
sourceId: '9e721628-018d-1000-38cc-5ea304d451c7',
|
||||||
|
canRead: true,
|
||||||
|
action: {
|
||||||
|
id: 276,
|
||||||
|
userIdentity: 'test',
|
||||||
|
timestamp: '02/12/2024 12:52:54 EST',
|
||||||
|
sourceId: '9e721628-018d-1000-38cc-5ea304d451c7',
|
||||||
|
sourceName: 'dummy',
|
||||||
|
sourceType: 'ProcessGroup',
|
||||||
|
operation: 'Remove'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ActionDetails],
|
||||||
|
providers: [{ provide: MAT_DIALOG_DATA, useValue: data }]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(ActionDetails);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* 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, Inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import {
|
||||||
|
ActionEntity,
|
||||||
|
ConfigureActionDetails,
|
||||||
|
ConnectionActionDetails,
|
||||||
|
ExtensionDetails,
|
||||||
|
MoveActionDetails,
|
||||||
|
PurgeActionDetails,
|
||||||
|
RemoteProcessGroupDetails
|
||||||
|
} from '../../../state/flow-configuration-history-listing';
|
||||||
|
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||||
|
import { PipesModule } from '../../../../../pipes/pipes.module';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'action-details',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, MatDialogModule, PipesModule, MatButtonModule],
|
||||||
|
templateUrl: './action-details.component.html',
|
||||||
|
styleUrls: ['./action-details.component.scss']
|
||||||
|
})
|
||||||
|
export class ActionDetails {
|
||||||
|
constructor(
|
||||||
|
@Inject(MAT_DIALOG_DATA) public actionEntity: ActionEntity,
|
||||||
|
private nifiCommon: NiFiCommon
|
||||||
|
) {}
|
||||||
|
|
||||||
|
isRemoteProcessGroup(actionEntity: ActionEntity): boolean {
|
||||||
|
return actionEntity.action.sourceType === 'RemoteProcessGroup';
|
||||||
|
}
|
||||||
|
|
||||||
|
getRemoteProcessGroupDetails(actionEntity: ActionEntity): RemoteProcessGroupDetails | null {
|
||||||
|
if (!this.isRemoteProcessGroup(actionEntity)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return actionEntity.action.componentDetails as RemoteProcessGroupDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
getExtensionDetails(actionEntity: ActionEntity): ExtensionDetails | null {
|
||||||
|
if (this.isRemoteProcessGroup(actionEntity)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return actionEntity.action.componentDetails as ExtensionDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigureActionDetails(actionEntity: ActionEntity): ConfigureActionDetails | null {
|
||||||
|
if (actionEntity.action.operation !== 'Configure') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return actionEntity.action.actionDetails as ConfigureActionDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
getConnectActionDetails(actionEntity: ActionEntity): ConnectionActionDetails | null {
|
||||||
|
if (!['Connect', 'Disconnect'].includes(actionEntity.action.operation)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return actionEntity.action.actionDetails as ConnectionActionDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMoveActionDetails(actionEntity: ActionEntity): MoveActionDetails | null {
|
||||||
|
if (actionEntity.action.operation !== 'Move') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return actionEntity.action.actionDetails as MoveActionDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPurgeActionDetails(actionEntity: ActionEntity): PurgeActionDetails | null {
|
||||||
|
if (actionEntity.action.operation !== 'Purge') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return actionEntity.action.actionDetails as PurgeActionDetails;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<ng-container *ngIf="historyListingState$ | async; let state">
|
||||||
|
<div *ngIf="isInitialLoading(state); else loaded">
|
||||||
|
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #loaded>
|
||||||
|
<div class="flow-configuration-history-listing flex flex-col h-full gap-y-2">
|
||||||
|
<div class="flex align-middle justify-between">
|
||||||
|
<div class="flow-configuration-filter flex-1">
|
||||||
|
<form [formGroup]="filterForm">
|
||||||
|
<div class="flex pt-2 gap-1 items-baseline">
|
||||||
|
<div>
|
||||||
|
<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">
|
||||||
|
<ng-container *ngFor="let option of filterableColumns">
|
||||||
|
<mat-option [value]="option.key"> {{ option.label }} </mat-option>
|
||||||
|
</ng-container>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="flex ml-6 gap-x-2">
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Date Range</mat-label>
|
||||||
|
<mat-date-range-input [rangePicker]="picker">
|
||||||
|
<input
|
||||||
|
matStartDate
|
||||||
|
formControlName="filterStartDate"
|
||||||
|
placeholder="Start date"
|
||||||
|
title="The start date in the format 'mm/dd/yyyy'. Must also specify start time." />
|
||||||
|
<input
|
||||||
|
matEndDate
|
||||||
|
formControlName="filterEndDate"
|
||||||
|
placeholder="End date"
|
||||||
|
title="The end date in the format 'mm/dd/yyyy'. Must also specify end time." />
|
||||||
|
</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>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Start Time ({{ (about$ | async)?.timezone }})</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="time"
|
||||||
|
step="1"
|
||||||
|
formControlName="filterStartTime"
|
||||||
|
placeholder="hh:mm:ss"
|
||||||
|
title="The start time in the format 'hh:mm:ss'. Must also specify start date." />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>End Time ({{ (about$ | async)?.timezone }})</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="time"
|
||||||
|
step="1"
|
||||||
|
formControlName="filterEndTime"
|
||||||
|
placeholder="hh:mm:ss"
|
||||||
|
title="The end time in the format 'hh:mm:ss'. Must also specify end date." />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex ml-4">
|
||||||
|
<a (click)="resetFilter($event)">Clear Filter</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4" *ngIf="(currentUser$ | async)?.controllerPermissions?.canWrite">
|
||||||
|
<button class="nifi-button" (click)="purgeHistoryClicked()">
|
||||||
|
<i class="fa fa-eraser"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<flow-configuration-history-table
|
||||||
|
[historyActions]="state.actions"
|
||||||
|
[selectedHistoryActionId]="selectedHistoryId$ | async"
|
||||||
|
(moreDetailsClicked)="openMoreDetails($event)"
|
||||||
|
(selectionChanged)="selectionChanged($event)"
|
||||||
|
(sortChanged)="sortChanged($event)"></flow-configuration-history-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between align-middle">
|
||||||
|
<div class="refresh-container flex items-center gap-x-2">
|
||||||
|
<button class="nifi-button" (click)="refresh()">
|
||||||
|
<i class="fa fa-refresh" [class.fa-spin]="state.status === 'loading'"></i>
|
||||||
|
</button>
|
||||||
|
<div>Last updated:</div>
|
||||||
|
<div class="refresh-timestamp">{{ state.loadedTimestamp }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<mat-paginator
|
||||||
|
[length]="state.total"
|
||||||
|
[pageSize]="pageSize"
|
||||||
|
[pageIndex]="getPageIndex()"
|
||||||
|
[hidePageSize]="true"
|
||||||
|
[showFirstLastButtons]="true"
|
||||||
|
(page)="paginationChanged($event)"></mat-paginator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*!
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.flow-configuration-history-listing {
|
||||||
|
.mdc-text-field__input::-webkit-calendar-picker-indicator {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { FlowConfigurationHistoryListing } from './flow-configuration-history-listing.component';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { initialHistoryState } from '../../state/flow-configuration-history-listing/flow-configuration-history-listing.reducer';
|
||||||
|
|
||||||
|
describe('FlowConfigurationHistoryListing', () => {
|
||||||
|
let component: FlowConfigurationHistoryListing;
|
||||||
|
let fixture: ComponentFixture<FlowConfigurationHistoryListing>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [FlowConfigurationHistoryListing],
|
||||||
|
providers: [provideMockStore({ initialState: initialHistoryState })]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(FlowConfigurationHistoryListing);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,317 @@
|
||||||
|
/*
|
||||||
|
* 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, DestroyRef, inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import * as HistoryActions from '../../state/flow-configuration-history-listing/flow-configuration-history-listing.actions';
|
||||||
|
import { loadHistory } from '../../state/flow-configuration-history-listing/flow-configuration-history-listing.actions';
|
||||||
|
import {
|
||||||
|
ActionEntity,
|
||||||
|
FlowConfigurationHistoryListingState,
|
||||||
|
HistoryQueryRequest
|
||||||
|
} from '../../state/flow-configuration-history-listing';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import {
|
||||||
|
selectedHistoryItem,
|
||||||
|
selectFlowConfigurationHistoryListingState,
|
||||||
|
selectHistoryQuery
|
||||||
|
} from '../../state/flow-configuration-history-listing/flow-configuration-history-listing.selectors';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
import { initialHistoryState } from '../../state/flow-configuration-history-listing/flow-configuration-history-listing.reducer';
|
||||||
|
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||||
|
import { FlowConfigurationHistoryTable } from './flow-configuration-history-table/flow-configuration-history-table.component';
|
||||||
|
import { Sort } from '@angular/material/sort';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { isDefinedAndNotNull } from '../../../../state/shared';
|
||||||
|
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
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 { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
|
import { selectAbout } from '../../../../state/about/about.selectors';
|
||||||
|
import { loadAbout } from '../../../../state/about/about.actions';
|
||||||
|
import { debounceTime } from 'rxjs';
|
||||||
|
import { NiFiCommon } from '../../../../service/nifi-common.service';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { selectCurrentUser } from '../../../../state/current-user/current-user.selectors';
|
||||||
|
|
||||||
|
interface FilterableColumn {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'flow-configuration-history-listing',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
NgxSkeletonLoaderModule,
|
||||||
|
MatPaginatorModule,
|
||||||
|
FlowConfigurationHistoryTable,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatOptionModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatDatepickerModule,
|
||||||
|
MatButtonModule
|
||||||
|
],
|
||||||
|
templateUrl: './flow-configuration-history-listing.component.html',
|
||||||
|
styleUrls: ['./flow-configuration-history-listing.component.scss']
|
||||||
|
})
|
||||||
|
export class FlowConfigurationHistoryListing implements OnInit, OnDestroy {
|
||||||
|
private static readonly DEFAULT_START_TIME: string = '00:00:00';
|
||||||
|
private static readonly DEFAULT_END_TIME: string = '23:59:59';
|
||||||
|
private static readonly TIME_REGEX = /^([0-1]\d|2[0-3]):([0-5]\d):([0-5]\d)$/;
|
||||||
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
historyListingState$ = this.store.select(selectFlowConfigurationHistoryListingState);
|
||||||
|
selectedHistoryId$ = this.store.select(selectedHistoryItem);
|
||||||
|
queryRequest$ = this.store.select(selectHistoryQuery);
|
||||||
|
about$ = this.store.select(selectAbout);
|
||||||
|
currentUser$ = this.store.select(selectCurrentUser);
|
||||||
|
|
||||||
|
pageSize = 50;
|
||||||
|
queryRequest: HistoryQueryRequest = {
|
||||||
|
count: this.pageSize,
|
||||||
|
offset: 0,
|
||||||
|
startDate: this.getFormattedStartDateTime(),
|
||||||
|
endDate: this.getFormattedEndDateTime()
|
||||||
|
};
|
||||||
|
|
||||||
|
filterForm: FormGroup;
|
||||||
|
filterableColumns: FilterableColumn[] = [
|
||||||
|
{ key: 'sourceId', label: 'id' },
|
||||||
|
{ key: 'userIdentity', label: 'user' }
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private store: Store<FlowConfigurationHistoryListingState>,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private nifiCommon: NiFiCommon
|
||||||
|
) {
|
||||||
|
this.queryRequest$
|
||||||
|
.pipe(takeUntilDestroyed(), isDefinedAndNotNull())
|
||||||
|
.subscribe((queryRequest) => (this.queryRequest = queryRequest));
|
||||||
|
|
||||||
|
const now: Date = new Date();
|
||||||
|
const twoWeeksAgo: Date = new Date(now.getTime() - 1000 * 60 * 60 * 24 * 14);
|
||||||
|
this.filterForm = this.formBuilder.group({
|
||||||
|
filterTerm: '',
|
||||||
|
filterColumn: this.filterableColumns[0].key,
|
||||||
|
filterStartDate: new FormControl(twoWeeksAgo),
|
||||||
|
filterStartTime: new FormControl(FlowConfigurationHistoryListing.DEFAULT_START_TIME, [
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(FlowConfigurationHistoryListing.TIME_REGEX)
|
||||||
|
]),
|
||||||
|
filterEndDate: new FormControl(now),
|
||||||
|
filterEndTime: new FormControl(FlowConfigurationHistoryListing.DEFAULT_END_TIME, [
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(FlowConfigurationHistoryListing.TIME_REGEX)
|
||||||
|
])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.store.dispatch(HistoryActions.resetHistoryState());
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.refresh();
|
||||||
|
this.store.dispatch(loadAbout());
|
||||||
|
|
||||||
|
this.onFormChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialLoading(state: FlowConfigurationHistoryListingState): boolean {
|
||||||
|
return state.loadedTimestamp == initialHistoryState.loadedTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.store.dispatch(HistoryActions.loadHistory({ request: this.queryRequest }));
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationChanged(pageEvent: PageEvent): void {
|
||||||
|
// Initiate the call to the backend for the requested page of data
|
||||||
|
this.store.dispatch(
|
||||||
|
HistoryActions.loadHistory({
|
||||||
|
request: {
|
||||||
|
...this.queryRequest,
|
||||||
|
count: this.pageSize,
|
||||||
|
offset: pageEvent.pageIndex * this.pageSize
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// clear out any selection
|
||||||
|
this.store.dispatch(HistoryActions.clearHistorySelection());
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionChanged(historyItem: ActionEntity) {
|
||||||
|
if (historyItem) {
|
||||||
|
this.store.dispatch(HistoryActions.selectHistoryItem({ request: { id: historyItem.id } }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortChanged(sort: Sort) {
|
||||||
|
if (this.queryRequest?.sortOrder !== sort.active || this.queryRequest.sortColumn !== sort.direction) {
|
||||||
|
// always reset the pagination when changing sort
|
||||||
|
this.store.dispatch(
|
||||||
|
HistoryActions.loadHistory({
|
||||||
|
request: {
|
||||||
|
...this.queryRequest,
|
||||||
|
count: this.pageSize,
|
||||||
|
offset: 0,
|
||||||
|
sortColumn: sort.active,
|
||||||
|
sortOrder: sort.direction ? `${sort.direction}` : 'asc'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// clear out any selection
|
||||||
|
this.store.dispatch(HistoryActions.clearHistorySelection());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageIndex(): number {
|
||||||
|
if (this.queryRequest.offset >= 0) {
|
||||||
|
return this.queryRequest.offset / this.pageSize;
|
||||||
|
} else return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFormChanges() {
|
||||||
|
this.filterForm.valueChanges
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef), debounceTime(300))
|
||||||
|
.subscribe((formValue) => {
|
||||||
|
if (!this.filterForm.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// clear out any selection
|
||||||
|
this.store.dispatch(HistoryActions.clearHistorySelection());
|
||||||
|
|
||||||
|
// always reset the pagination state when a filter changes
|
||||||
|
const historyRequest: HistoryQueryRequest = {
|
||||||
|
...this.queryRequest,
|
||||||
|
offset: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formValue.filterTerm) {
|
||||||
|
if (formValue.filterColumn === 'sourceId') {
|
||||||
|
historyRequest.sourceId = formValue.filterTerm;
|
||||||
|
delete historyRequest.userIdentity;
|
||||||
|
} else {
|
||||||
|
historyRequest.userIdentity = formValue.filterTerm;
|
||||||
|
delete historyRequest.sourceId;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete historyRequest.sourceId;
|
||||||
|
delete historyRequest.userIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start: Date = new Date();
|
||||||
|
if (formValue.filterStartDate) {
|
||||||
|
historyRequest.startDate = this.getFormattedStartDateTime(
|
||||||
|
formValue.filterStartDate,
|
||||||
|
formValue.filterStartTime
|
||||||
|
);
|
||||||
|
start = new Date(historyRequest.startDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
let end: Date = new Date(0);
|
||||||
|
if (formValue.filterEndDate) {
|
||||||
|
historyRequest.endDate = this.getFormattedEndDateTime(
|
||||||
|
formValue.filterEndDate,
|
||||||
|
formValue.filterEndTime
|
||||||
|
);
|
||||||
|
end = new Date(historyRequest.endDate);
|
||||||
|
}
|
||||||
|
if (this.queryChanged(historyRequest) && start.getTime() <= end.getTime()) {
|
||||||
|
this.store.dispatch(
|
||||||
|
loadHistory({
|
||||||
|
request: historyRequest
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private queryChanged(historyRequest: HistoryQueryRequest) {
|
||||||
|
const before: HistoryQueryRequest = this.queryRequest;
|
||||||
|
const proposed: HistoryQueryRequest = historyRequest;
|
||||||
|
|
||||||
|
return (
|
||||||
|
proposed.endDate !== before.endDate ||
|
||||||
|
proposed.startDate !== before.startDate ||
|
||||||
|
proposed.sourceId != before.sourceId ||
|
||||||
|
proposed.userIdentity != before.userIdentity
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetFilter(event: MouseEvent) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const now = new Date();
|
||||||
|
const twoWeeksAgo: Date = new Date(now.getTime() - 1000 * 60 * 60 * 24 * 14);
|
||||||
|
this.filterForm.reset({
|
||||||
|
filterTerm: '',
|
||||||
|
filterColumn: 'sourceId',
|
||||||
|
filterStartTime: FlowConfigurationHistoryListing.DEFAULT_START_TIME,
|
||||||
|
filterStartDate: twoWeeksAgo,
|
||||||
|
filterEndTime: FlowConfigurationHistoryListing.DEFAULT_END_TIME,
|
||||||
|
filterEndDate: now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openMoreDetails(actionEntity: ActionEntity) {
|
||||||
|
this.store.dispatch(
|
||||||
|
HistoryActions.openMoreDetailsDialog({
|
||||||
|
request: actionEntity
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
purgeHistoryClicked() {
|
||||||
|
this.store.dispatch(HistoryActions.openPurgeHistoryDialog());
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormatDateTime(date: Date, time: string): string {
|
||||||
|
let formatted = this.nifiCommon.formatDateTime(date);
|
||||||
|
// 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];
|
||||||
|
|
||||||
|
// combine the pieces into the format the api requires
|
||||||
|
formatted = `${formattedStartDate} ${time}`;
|
||||||
|
}
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormattedStartDateTime(date?: Date, time?: string): string {
|
||||||
|
const now = new Date();
|
||||||
|
const twoWeeksAgo: Date = new Date(now.getTime() - 1000 * 60 * 60 * 24 * 14);
|
||||||
|
const d: Date = date ? date : twoWeeksAgo;
|
||||||
|
const t: string = time ? time : FlowConfigurationHistoryListing.DEFAULT_START_TIME;
|
||||||
|
return this.getFormatDateTime(d, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormattedEndDateTime(date?: Date, time?: string): string {
|
||||||
|
const d: Date = date ? date : new Date();
|
||||||
|
const t: string = time ? time : FlowConfigurationHistoryListing.DEFAULT_END_TIME;
|
||||||
|
return this.getFormatDateTime(d, t);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="flow-configuration-history-table flex-1 relative h-full w-full">
|
||||||
|
<div class="listing-table overflow-y-auto border absolute inset-0">
|
||||||
|
<table
|
||||||
|
mat-table
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
matSort
|
||||||
|
matSortDisableClear
|
||||||
|
(matSortChange)="sortData($event)"
|
||||||
|
[matSortActive]="initialSortColumn"
|
||||||
|
[matSortDirection]="initialSortDirection">
|
||||||
|
<ng-container matColumnDef="moreDetails">
|
||||||
|
<th mat-header-cell *matHeaderCellDef></th>
|
||||||
|
<td mat-cell *matCellDef="let item">
|
||||||
|
<ng-container>
|
||||||
|
<div class="flex items-center gap-x-3">
|
||||||
|
<div
|
||||||
|
*ngIf="canRead(item)"
|
||||||
|
class="pointer fa fa-info-circle"
|
||||||
|
title="View Details"
|
||||||
|
(click)="moreDetails(item)"></div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Name Column -->
|
||||||
|
<ng-container matColumnDef="timestamp">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||||
|
<div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Date/Time</div>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let item" [title]="formatTimestamp(item)">
|
||||||
|
{{ formatTimestamp(item) }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Name Column -->
|
||||||
|
<ng-container matColumnDef="sourceName">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||||
|
<div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Name</div>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let item" [title]="formatName(item)">
|
||||||
|
<span [class.blank]="!item.action?.sourceName">{{ formatName(item) }}</span>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="sourceType">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||||
|
<div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Type</div>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let item" [title]="formatType(item)">
|
||||||
|
{{ formatType(item) }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="operation">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||||
|
<div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">Operation</div>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let item" [title]="formatOperation(item)">
|
||||||
|
{{ formatOperation(item) }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="userIdentity">
|
||||||
|
<th mat-header-cell *matHeaderCellDef mat-sort-header>
|
||||||
|
<div class="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">User</div>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let item" [title]="formatUser(item)">
|
||||||
|
{{ formatUser(item) }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||||
|
<tr
|
||||||
|
mat-row
|
||||||
|
*matRowDef="let row; let even = even; columns: displayedColumns"
|
||||||
|
[class.even]="even"
|
||||||
|
(click)="select(row)"
|
||||||
|
[class.unset]="!canRead(row)"
|
||||||
|
[class.selected]="isSelected(row)"></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*!
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.flow-configuration-history-table {
|
||||||
|
.listing-table {
|
||||||
|
.mat-column-moreDetails {
|
||||||
|
width: 32px;
|
||||||
|
min-width: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FlowConfigurationHistoryTable } from './flow-configuration-history-table.component';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
describe('FlowConfigurationHistoryTable', () => {
|
||||||
|
let component: FlowConfigurationHistoryTable;
|
||||||
|
let fixture: ComponentFixture<FlowConfigurationHistoryTable>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [FlowConfigurationHistoryTable, NoopAnimationsModule]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(FlowConfigurationHistoryTable);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||||
|
import { MatSortModule, Sort } from '@angular/material/sort';
|
||||||
|
import { ActionEntity } from '../../../state/flow-configuration-history-listing';
|
||||||
|
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'flow-configuration-history-table',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, MatTableModule, MatSortModule],
|
||||||
|
templateUrl: './flow-configuration-history-table.component.html',
|
||||||
|
styleUrls: ['./flow-configuration-history-table.component.scss']
|
||||||
|
})
|
||||||
|
export class FlowConfigurationHistoryTable {
|
||||||
|
@Input() selectedHistoryActionId: number | null = null;
|
||||||
|
@Input() initialSortColumn: 'timestamp' | 'sourceName' | 'sourceType' | 'operation' | 'userIdentity' = 'timestamp';
|
||||||
|
@Input() initialSortDirection: 'asc' | 'desc' = 'desc';
|
||||||
|
|
||||||
|
@Input() set historyActions(historyActions: ActionEntity[]) {
|
||||||
|
if (historyActions) {
|
||||||
|
this.dataSource.data = historyActions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Output() selectionChanged: EventEmitter<ActionEntity> = new EventEmitter<ActionEntity>();
|
||||||
|
@Output() sortChanged: EventEmitter<Sort> = new EventEmitter<Sort>();
|
||||||
|
@Output() moreDetailsClicked: EventEmitter<ActionEntity> = new EventEmitter<ActionEntity>();
|
||||||
|
|
||||||
|
activeSort: Sort = {
|
||||||
|
active: this.initialSortColumn,
|
||||||
|
direction: this.initialSortDirection
|
||||||
|
};
|
||||||
|
|
||||||
|
displayedColumns: string[] = ['moreDetails', 'timestamp', 'sourceName', 'sourceType', 'operation', 'userIdentity'];
|
||||||
|
dataSource: MatTableDataSource<ActionEntity> = new MatTableDataSource<ActionEntity>();
|
||||||
|
|
||||||
|
constructor(private nifiCommon: NiFiCommon) {}
|
||||||
|
|
||||||
|
sortData(sort: Sort) {
|
||||||
|
this.sortChanged.next(sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
canRead(item: ActionEntity): boolean {
|
||||||
|
return item.canRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
private format(item: ActionEntity, property: string): string {
|
||||||
|
if (this.canRead(item) && Object.hasOwn(item.action, property)) {
|
||||||
|
const value = (item.action as any)[property];
|
||||||
|
if (!value) {
|
||||||
|
return 'Empty String Set';
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return 'Not Authorized';
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTimestamp(item: ActionEntity): string {
|
||||||
|
return this.format(item, 'timestamp');
|
||||||
|
}
|
||||||
|
|
||||||
|
formatName(item: ActionEntity): string {
|
||||||
|
return this.format(item, 'sourceName');
|
||||||
|
}
|
||||||
|
|
||||||
|
formatType(item: ActionEntity): string {
|
||||||
|
return this.format(item, 'sourceType');
|
||||||
|
}
|
||||||
|
|
||||||
|
formatOperation(item: ActionEntity): string {
|
||||||
|
return this.format(item, 'operation');
|
||||||
|
}
|
||||||
|
|
||||||
|
formatUser(item: ActionEntity): string {
|
||||||
|
return this.format(item, 'userIdentity');
|
||||||
|
}
|
||||||
|
|
||||||
|
select(item: ActionEntity) {
|
||||||
|
this.selectionChanged.next(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelected(item: ActionEntity): boolean {
|
||||||
|
if (this.selectedHistoryActionId) {
|
||||||
|
return item.id === this.selectedHistoryActionId;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
moreDetails(item: ActionEntity) {
|
||||||
|
this.moreDetailsClicked.next(item);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
<!--
|
||||||
|
~ 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="purge-history">
|
||||||
|
<h2 mat-dialog-title>Purge History</h2>
|
||||||
|
<form class="purge-history-form" [formGroup]="purgeHistoryForm">
|
||||||
|
<div class="purge-history-content h-full">
|
||||||
|
<mat-dialog-content class="h-full">
|
||||||
|
<div class="panel-content flex h-full w-full gap-y-4 gap-x-4 pt-4">
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Before Date</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
[matDatepicker]="endDatePicker"
|
||||||
|
formControlName="endDate"
|
||||||
|
placeholder="mm/dd/yyyy"
|
||||||
|
title="The end date in the format 'mm/dd/yyyy'. Must also specify end time." />
|
||||||
|
<mat-datepicker-toggle matIconSuffix [for]="endDatePicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #endDatePicker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Before Time ({{ (about$ | async)?.timezone }})</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
type="time"
|
||||||
|
step="1"
|
||||||
|
formControlName="endTime"
|
||||||
|
placeholder="hh:mm:ss"
|
||||||
|
title="The start time in the format 'hh:mm:ss'. Must also specify start date." />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<mat-dialog-actions align="end">
|
||||||
|
<button color="primary" mat-stroked-button mat-dialog-close>Cancel</button>
|
||||||
|
<button
|
||||||
|
color="primary"
|
||||||
|
(click)="submit()"
|
||||||
|
[disabled]="!purgeHistoryForm.valid"
|
||||||
|
mat-raised-button
|
||||||
|
mat-dialog-close>
|
||||||
|
Ok
|
||||||
|
</button>
|
||||||
|
</mat-dialog-actions>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.purge-history {
|
||||||
|
.purge-history-content {
|
||||||
|
.mdc-dialog__content {
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-text-field__input::-webkit-calendar-picker-indicator {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PurgeHistory } from './purge-history.component';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { initialHistoryState } from '../../../state/flow-configuration-history-listing/flow-configuration-history-listing.reducer';
|
||||||
|
import { MatNativeDateModule } from '@angular/material/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
describe('PurgeHistory', () => {
|
||||||
|
let component: PurgeHistory;
|
||||||
|
let fixture: ComponentFixture<PurgeHistory>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [PurgeHistory, MatNativeDateModule, NoopAnimationsModule],
|
||||||
|
providers: [provideMockStore({ initialState: initialHistoryState })]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(PurgeHistory);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* 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, Output } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
FlowConfigurationHistoryListingState,
|
||||||
|
PurgeHistoryRequest
|
||||||
|
} from '../../../state/flow-configuration-history-listing';
|
||||||
|
import { NiFiCommon } from '../../../../../service/nifi-common.service';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
|
import { selectAbout } from '../../../../../state/about/about.selectors';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'purge-history',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, MatDialogModule, MatButtonModule, ReactiveFormsModule, MatInputModule, MatDatepickerModule],
|
||||||
|
templateUrl: './purge-history.component.html',
|
||||||
|
styleUrls: ['./purge-history.component.scss']
|
||||||
|
})
|
||||||
|
export class PurgeHistory {
|
||||||
|
private static readonly DEFAULT_PURGE_TIME: string = '00:00:00';
|
||||||
|
private static readonly TIME_REGEX = /^([0-1]\d|2[0-3]):([0-5]\d):([0-5]\d)$/;
|
||||||
|
purgeHistoryForm: FormGroup;
|
||||||
|
about$ = this.store.select(selectAbout);
|
||||||
|
|
||||||
|
@Output() submitPurgeRequest: EventEmitter<PurgeHistoryRequest> = new EventEmitter<PurgeHistoryRequest>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private nifiCommon: NiFiCommon,
|
||||||
|
private store: Store<FlowConfigurationHistoryListingState>
|
||||||
|
) {
|
||||||
|
const now: Date = new Date();
|
||||||
|
const aMonthAgo: Date = new Date();
|
||||||
|
aMonthAgo.setMonth(now.getMonth() - 1);
|
||||||
|
|
||||||
|
this.purgeHistoryForm = this.formBuilder.group({
|
||||||
|
endDate: new FormControl(aMonthAgo, Validators.required),
|
||||||
|
endTime: new FormControl(PurgeHistory.DEFAULT_PURGE_TIME, [
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(PurgeHistory.TIME_REGEX)
|
||||||
|
])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
const formEndDate = this.purgeHistoryForm.get('endDate')?.value;
|
||||||
|
const formEndTime = this.purgeHistoryForm.get('endTime')?.value;
|
||||||
|
|
||||||
|
const request: PurgeHistoryRequest = {
|
||||||
|
endDate: formEndDate
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formEndDate && formEndTime) {
|
||||||
|
const formatted = this.nifiCommon.formatDateTime(formEndDate);
|
||||||
|
// 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 = formEndTime;
|
||||||
|
if (!endTime) {
|
||||||
|
endTime = PurgeHistory.DEFAULT_PURGE_TIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
// combine the pieces into the format the api requires
|
||||||
|
request.endDate = `${formattedEndDate} ${endTime}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitPurgeRequest.next(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,7 +47,7 @@
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>Start Time ({{ timezone }})</mat-label>
|
<mat-label>Start Time ({{ timezone }})</mat-label>
|
||||||
<input matInput type="text" formControlName="startTime" />
|
<input matInput type="time" step="1" formControlName="startTime" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>Minimum File Size</mat-label>
|
<mat-label>Minimum File Size</mat-label>
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>End Time ({{ timezone }})</mat-label>
|
<mat-label>End Time ({{ timezone }})</mat-label>
|
||||||
<input matInput type="text" formControlName="endTime" />
|
<input matInput type="time" step="1" formControlName="endTime" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>Maximum File Size</mat-label>
|
<mat-label>Maximum File Size</mat-label>
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
import { Component, EventEmitter, Inject, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, Inject, Input, Output } from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||||
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
@ -54,6 +54,7 @@ export class ProvenanceSearchDialog {
|
||||||
public static readonly MAX_RESULTS: number = 1000;
|
public static readonly MAX_RESULTS: number = 1000;
|
||||||
private static readonly DEFAULT_START_TIME: string = '00:00:00';
|
private static readonly DEFAULT_START_TIME: string = '00:00:00';
|
||||||
private static readonly DEFAULT_END_TIME: string = '23:59:59';
|
private static readonly DEFAULT_END_TIME: string = '23:59:59';
|
||||||
|
private static readonly TIME_REGEX = /^([0-1]\d|2[0-3]):([0-5]\d):([0-5]\d)$/;
|
||||||
|
|
||||||
provenanceOptionsForm: FormGroup;
|
provenanceOptionsForm: FormGroup;
|
||||||
|
|
||||||
|
@ -106,9 +107,15 @@ export class ProvenanceSearchDialog {
|
||||||
|
|
||||||
this.provenanceOptionsForm = this.formBuilder.group({
|
this.provenanceOptionsForm = this.formBuilder.group({
|
||||||
startDate: new FormControl(startDate),
|
startDate: new FormControl(startDate),
|
||||||
startTime: new FormControl(startTime),
|
startTime: new FormControl(startTime, [
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(ProvenanceSearchDialog.TIME_REGEX)
|
||||||
|
]),
|
||||||
endDate: new FormControl(endDate),
|
endDate: new FormControl(endDate),
|
||||||
endTime: new FormControl(endTime),
|
endTime: new FormControl(endTime, [
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(ProvenanceSearchDialog.TIME_REGEX)
|
||||||
|
]),
|
||||||
minFileSize: new FormControl(minFileSize),
|
minFileSize: new FormControl(minFileSize),
|
||||||
maxFileSize: new FormControl(maxFileSize)
|
maxFileSize: new FormControl(maxFileSize)
|
||||||
});
|
});
|
||||||
|
|
|
@ -89,7 +89,7 @@
|
||||||
<i class="fa fa-fw fa-cubes mr-2"></i>
|
<i class="fa fa-fw fa-cubes mr-2"></i>
|
||||||
Cluster
|
Cluster
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item class="global-menu-item">
|
<button mat-menu-item class="global-menu-item" [routerLink]="['/flow-configuration-history']">
|
||||||
<i class="fa fa-fw fa-history mr-2"></i>
|
<i class="fa fa-fw fa-history mr-2"></i>
|
||||||
Flow Configuration History
|
Flow Configuration History
|
||||||
</button>
|
</button>
|
||||||
|
|
Loading…
Reference in New Issue