[NIFI-12415] Counters (#8077)

* [NIFI-12415] Counters page
* populate counter the table.
* support counter reset.
* filtering by name and by context
* sorting, including initial sort
* added basic tests

* Formatted with Prettier

* review feedback - removing unused things.

* align mat-table usage to a common style

* disable the Counter menu item if the user doesn't have counterPermissions.canRead

This closes #8077
This commit is contained in:
Rob Fellows 2023-11-29 18:00:28 -05:00 committed by GitHub
parent 05575364a3
commit d14686cb3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1219 additions and 294 deletions

View File

@ -7,6 +7,7 @@
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test --karma-config=karma.conf.js --watch=false",
"prettier": "prettier --config .prettierrc . --check",
"prettier-format": "prettier --config .prettierrc . --write",
"ci": "npm ci --ignore-scripts"
},

View File

@ -37,6 +37,11 @@ const routes: Routes = [
(m) => m.ParameterContextsModule
)
},
{
path: 'counters',
canMatch: [authGuard],
loadChildren: () => import('./pages/counters/feature/counters.module').then((m) => m.CountersModule)
},
{
path: '',
canMatch: [authGuard],

View File

@ -64,62 +64,66 @@
<i class="fa fa-navicon"></i>
</button>
<mat-menu #globalMenu="matMenu" xPosition="before">
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-table mr-2"></i>
Summary
</button>
<button mat-menu-item class="global-menu-item">
<i class="icon fa-fw icon-counter mr-2"></i>
Counter
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-sticky-note-o mr-2"></i>
Bulletin Board
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item">
<i class="icon fa-fw icon-provenance mr-2"></i>
Data Provenance
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item" [routerLink]="['/settings']">
<i class="fa fa-fw fa-wrench mr-2"></i>
Controller Settings
</button>
<button mat-menu-item class="global-menu-item" [routerLink]="['/parameter-contexts']">
<i class="fa fa-fw mr-2"></i>
Parameter Contexts
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-cubes mr-2"></i>
Cluster
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-history mr-2"></i>
Flow Configuration History
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-area-chart mr-2"></i>
Node Status History
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-users mr-2"></i>
Users
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-key mr-2"></i>
Policies
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-question-circle mr-2"></i>
Help
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-info-circle mr-2"></i>
About
</button>
<ng-container *ngIf="currentUser$ | async as user">
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-table mr-2"></i>
Summary
</button>
<button mat-menu-item class="global-menu-item"
[routerLink]="['/counters']"
[disabled]="!user.countersPermissions.canRead">
<i class="icon fa-fw icon-counter mr-2"></i>
Counter
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-sticky-note-o mr-2"></i>
Bulletin Board
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item">
<i class="icon fa-fw icon-provenance mr-2"></i>
Data Provenance
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item" [routerLink]="['/settings']">
<i class="fa fa-fw fa-wrench mr-2"></i>
Controller Settings
</button>
<button mat-menu-item class="global-menu-item" [routerLink]="['/parameter-contexts']">
<i class="fa fa-fw mr-2"></i>
Parameter Contexts
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-cubes mr-2"></i>
Cluster
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-history mr-2"></i>
Flow Configuration History
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-area-chart mr-2"></i>
Node Status History
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-users mr-2"></i>
Users
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-key mr-2"></i>
Policies
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-question-circle mr-2"></i>
Help
</button>
<button mat-menu-item class="global-menu-item">
<i class="fa fa-fw fa-info-circle mr-2"></i>
About
</button>
</ng-container>
</mat-menu>
</div>
</div>

View File

@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { Counters } from './counters.component';
const routes: Routes = [
{
path: '',
component: Counters
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class CountersRoutingModule {}

View File

@ -0,0 +1,28 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<div class="p-4 flex flex-col h-screen justify-between gap-y-5">
<div class="flex justify-between">
<h3 class="text-xl bold counter-header">NiFi Counters</h3>
<button class="nifi-button" [routerLink]="['/']">
<i class="fa fa-times"></i>
</button>
</div>
<div class="flex-1">
<counter-listing></counter-listing>
</div>
</div>

View File

@ -0,0 +1,20 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.counter-header {
color: #728e9b;
}

View File

@ -0,0 +1,44 @@
/*
* 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 { Counters } from './counters.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../state/counter-listing/counter-listing.reducer';
import { CounterListing } from '../ui/counter-listing/counter-listing.component';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
describe('Counters', () => {
let component: Counters;
let fixture: ComponentFixture<Counters>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [Counters, CounterListing],
imports: [RouterModule, RouterTestingModule],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(Counters);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { NiFiState } from '../../../state';
import { startUserPolling, stopUserPolling } from '../../../state/user/user.actions';
@Component({
selector: 'counters',
templateUrl: './counters.component.html',
styleUrls: ['./counters.component.scss']
})
export class Counters implements OnInit, OnDestroy {
constructor(private store: Store<NiFiState>) {}
ngOnInit(): void {
this.store.dispatch(startUserPolling());
}
ngOnDestroy(): void {
this.store.dispatch(stopUserPolling());
}
}

View File

@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Counters } from './counters.component';
import { CountersRoutingModule } from './counters-routing.module';
import { StoreModule } from '@ngrx/store';
import { countersFeatureKey, reducers } from '../state';
import { EffectsModule } from '@ngrx/effects';
import { CounterListingEffects } from '../state/counter-listing/counter-listing.effects';
import { CounterListingModule } from '../ui/counter-listing/counter-listing.module';
import { ParameterContextListingModule } from '../../parameter-contexts/ui/parameter-context-listing/parameter-context-listing.module';
@NgModule({
declarations: [Counters],
exports: [Counters],
imports: [
CommonModule,
CountersRoutingModule,
StoreModule.forFeature(countersFeatureKey, reducers),
EffectsModule.forFeature(CounterListingEffects),
CounterListingModule,
ParameterContextListingModule
]
})
export class CountersModule {}

View File

@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Client } from '../../../service/client.service';
import { Observable } from 'rxjs';
import { CounterEntity, ResetCounterRequest } from '../state/counter-listing';
@Injectable({ providedIn: 'root' })
export class CountersService {
private static readonly API: string = '../nifi-api';
constructor(
private httpClient: HttpClient,
private client: Client
) {}
getCounters(): Observable<any> {
return this.httpClient.get(`${CountersService.API}/counters`);
}
resetCounter(counterResetRequest: ResetCounterRequest): Observable<any> {
return this.httpClient.put(`${CountersService.API}/counters/${counterResetRequest.counter.id}`, null);
}
}

View File

@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createAction, props } from '@ngrx/store';
import { LoadCounterListingResponse, ResetCounterRequest, ResetCounterSuccess } from './index';
const COUNTER_PREFIX: string = '[Counter Listing]';
export const loadCounters = createAction(`${COUNTER_PREFIX} Load Counter Listing`);
export const loadCountersSuccess = createAction(
`${COUNTER_PREFIX} Load Counter Listing Success`,
props<{ response: LoadCounterListingResponse }>()
);
export const counterListingApiError = createAction(
`${COUNTER_PREFIX} Load Counter Listing Errors`,
props<{ error: string }>()
);
export const promptCounterReset = createAction(
`${COUNTER_PREFIX} Prompt Counter Reset`,
props<{ request: ResetCounterRequest }>()
);
export const resetCounter = createAction(`${COUNTER_PREFIX} Reset Counter`, props<{ request: ResetCounterRequest }>());
export const resetCounterSuccess = createAction(
`${COUNTER_PREFIX} Reset Counter Success`,
props<{ response: ResetCounterSuccess }>()
);

View File

@ -0,0 +1,106 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { NiFiState } from '../../../../state';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';
import * as CounterListingActions from './counter-listing.actions';
import { catchError, from, map, of, switchMap, take, tap } from 'rxjs';
import { CountersService } from '../../service/counters.service';
import { MatDialog } from '@angular/material/dialog';
import { YesNoDialog } from '../../../../ui/common/yes-no-dialog/yes-no-dialog.component';
@Injectable()
export class CounterListingEffects {
constructor(
private actions$: Actions,
private store: Store<NiFiState>,
private router: Router,
private countersService: CountersService,
private dialog: MatDialog
) {}
loadCounters$ = createEffect(() =>
this.actions$.pipe(
ofType(CounterListingActions.loadCounters),
switchMap(() =>
from(this.countersService.getCounters()).pipe(
map((response) =>
CounterListingActions.loadCountersSuccess({
response: {
counters: response.counters.aggregateSnapshot.counters,
loadedTimestamp: response.counters.aggregateSnapshot.generated
}
})
),
catchError((error) =>
of(
CounterListingActions.counterListingApiError({
error: error.error
})
)
)
)
)
)
);
promptCounterReset$ = createEffect(
() =>
this.actions$.pipe(
ofType(CounterListingActions.promptCounterReset),
map((action) => action.request),
tap((request) => {
const dialogReference = this.dialog.open(YesNoDialog, {
data: {
title: 'Reset Counter',
message: `Reset counter '${request.counter.name}' to default value?`
},
panelClass: 'small-dialog'
});
dialogReference.componentInstance.yes.pipe(take(1)).subscribe(() => {
this.store.dispatch(
CounterListingActions.resetCounter({
request
})
);
});
})
),
{ dispatch: false }
);
resetCounter$ = createEffect(() =>
this.actions$.pipe(
ofType(CounterListingActions.resetCounter),
map((action) => action.request),
switchMap((request) =>
from(this.countersService.resetCounter(request)).pipe(
map((response) =>
CounterListingActions.resetCounterSuccess({
response
})
),
catchError((error) => of(CounterListingActions.counterListingApiError({ error: error.error })))
)
)
)
);
}

View File

@ -0,0 +1,61 @@
/*
* 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 { CounterListingState } from './index';
import { createReducer, on } from '@ngrx/store';
import { loadCounters, loadCountersSuccess, resetCounterSuccess } from './counter-listing.actions';
import { parameterContextListingApiError } from '../../../parameter-contexts/state/parameter-context-listing/parameter-context-listing.actions';
import { produce } from 'immer';
export const initialState: CounterListingState = {
counters: [],
saving: false,
loadedTimestamp: '',
error: null,
status: 'pending'
};
export const counterListingReducer = createReducer(
initialState,
on(loadCounters, (state) => ({
...state,
status: 'loading' as const
})),
on(loadCountersSuccess, (state, { response }) => ({
...state,
counters: response.counters,
loadedTimestamp: response.loadedTimestamp,
error: null,
status: 'success' as const
})),
on(parameterContextListingApiError, (state, { error }) => ({
...state,
saving: false,
error,
status: 'error' as const
})),
on(resetCounterSuccess, (state, { response }) => {
return produce(state, (draftState) => {
const index: number = draftState.counters.findIndex((c: any) => c.id === response.counter.id);
if (index > -1) {
draftState.counters[index] = {
...response.counter
};
}
});
})
);

View File

@ -0,0 +1,27 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createSelector } from '@ngrx/store';
import { countersFeatureKey, CountersState, selectCounterState } from '../index';
import { CounterListingState } from './index';
export const selectCounterListingState = createSelector(
selectCounterState,
(state: CountersState) => state[countersFeatureKey]
);
export const selectCounters = createSelector(selectCounterListingState, (state: CounterListingState) => state.counters);

View File

@ -0,0 +1,45 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface CounterEntity {
id: string;
context: string;
name: string;
valueCount: number;
value: string;
}
export interface CounterListingState {
counters: CounterEntity[];
saving: boolean;
loadedTimestamp: string;
error: string | null;
status: 'pending' | 'loading' | 'error' | 'success';
}
export interface LoadCounterListingResponse {
counters: CounterEntity[];
loadedTimestamp: string;
}
export interface ResetCounterRequest {
counter: CounterEntity;
}
export interface ResetCounterSuccess {
counter: CounterEntity;
}

View File

@ -0,0 +1,34 @@
/*
* 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 { CounterListingState } from './counter-listing';
import { Action, combineReducers, createFeatureSelector } from '@ngrx/store';
import { counterListingReducer } from './counter-listing/counter-listing.reducer';
export const countersFeatureKey = 'counters';
export interface CountersState {
[countersFeatureKey]: CounterListingState;
}
export function reducers(state: CountersState | undefined, action: Action) {
return combineReducers({
[countersFeatureKey]: counterListingReducer
})(state, action);
}
export const selectCounterState = createFeatureSelector<CountersState>(countersFeatureKey);

View File

@ -0,0 +1,42 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<ng-container *ngIf="counterListingState$ | async; let counterListingState">
<div *ngIf="isInitialLoading(counterListingState); else loaded">
<ngx-skeleton-loader count="3"></ngx-skeleton-loader>
</div>
<ng-template #loaded>
<div class="flex flex-col h-full gap-y-2">
<div class="flex-1" *ngIf="currentUser$ | async as user">
<counter-table
[counters]="counterListingState.counters"
[canModifyCounters]="user.countersPermissions.canWrite"
(resetCounter)="resetCounter($event)"></counter-table>
</div>
<div class="flex justify-between">
<div class="refresh-container flex items-center gap-x-2">
<button class="nifi-button" (click)="refreshCounterListing()">
<i class="fa fa-refresh" [class.fa-spin]="counterListingState.status === 'loading'"></i>
</button>
<div>Last updated:</div>
<div class="refresh-timestamp">{{ counterListingState.loadedTimestamp }}</div>
</div>
</div>
</div>
</ng-template>
</ng-container>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CounterListing } from './counter-listing.component';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { initialState } from '../../state/counter-listing/counter-listing.reducer';
describe('CounterListing', () => {
let component: CounterListing;
let fixture: ComponentFixture<CounterListing>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [CounterListing],
providers: [provideMockStore({ initialState })]
});
fixture = TestBed.createComponent(CounterListing);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,58 @@
/*
* 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, OnInit } from '@angular/core';
import { CounterEntity, CounterListingState } from '../../state/counter-listing';
import { Store } from '@ngrx/store';
import { loadCounters, promptCounterReset } from '../../state/counter-listing/counter-listing.actions';
import { selectCounterListingState } from '../../state/counter-listing/counter-listing.selectors';
import { initialState } from '../../state/counter-listing/counter-listing.reducer';
import { selectUser } from '../../../../state/user/user.selectors';
@Component({
selector: 'counter-listing',
templateUrl: './counter-listing.component.html',
styleUrls: ['./counter-listing.component.scss']
})
export class CounterListing implements OnInit {
counterListingState$ = this.store.select(selectCounterListingState);
currentUser$ = this.store.select(selectUser);
constructor(private store: Store<CounterListingState>) {}
ngOnInit(): void {
this.store.dispatch(loadCounters());
}
isInitialLoading(state: CounterListingState): boolean {
return state.loadedTimestamp == initialState.loadedTimestamp;
}
refreshCounterListing() {
this.store.dispatch(loadCounters());
}
resetCounter(entity: CounterEntity): void {
this.store.dispatch(
promptCounterReset({
request: {
counter: entity
}
})
);
}
}

View File

@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NgModule } from '@angular/core';
import { CounterListing } from './counter-listing.component';
import { CommonModule } from '@angular/common';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { CounterTable } from './counter-table/counter-table.component';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
@NgModule({
declarations: [CounterListing, CounterTable],
exports: [CounterListing],
imports: [
CommonModule,
NgxSkeletonLoaderModule,
MatTableModule,
MatSortModule,
MatInputModule,
ReactiveFormsModule,
MatSelectModule
]
})
export class CounterListingModule {}

View File

@ -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.
-->
<div class="counter-table h-full flex flex-col">
<div class="counter-table-filter-container">
<div class="value">Displaying {{ filteredCount }} of {{ totalCount }}</div>
<form [formGroup]="filterForm">
<div class="flex pt-2">
<div class="mr-2">
<mat-form-field>
<mat-label>Filter</mat-label>
<input matInput type="text" class="small" formControlName="filterTerm" />
</mat-form-field>
</div>
<div>
<mat-form-field>
<mat-label>Filter By</mat-label>
<mat-select formControlName="filterColumn">
<mat-option value="name"> name </mat-option>
<mat-option value="context"> context </mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</form>
</div>
<div class="flex-1 relative">
<div class="listing-table overflow-y-auto border absolute inset-0">
<table
mat-table
[dataSource]="dataSource"
matSort
matSortDisableClear
[matSortActive]="initialSortColumn"
[matSortDirection]="initialSortDirection">
<!-- Context column -->
<ng-container matColumnDef="context">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Context</th>
<td mat-cell *matCellDef="let item">
{{ formatContext(item) }}
</td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
<td mat-cell *matCellDef="let item">
{{ formatName(item) }}
</td>
</ng-container>
<!-- Value column -->
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Value</th>
<td mat-cell *matCellDef="let item">
{{ formatValue(item) }}
</td>
</ng-container>
<ng-container matColumnDef="reset">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell *matCellDef="let item">
<ng-container *ngIf="canModifyCounters">
<div class="flex items-center gap-x-3">
<div
class="pointer fa fa-undo"
title="Reset Counter"
*ngIf="canModifyCounters"
(click)="resetClicked(item, $event)"></div>
</div>
</ng-container>
</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"></tr>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,16 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -0,0 +1,52 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterTable } from './counter-table.component';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('CounterTableComponent', () => {
let component: CounterTable;
let fixture: ComponentFixture<CounterTable>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [CounterTable],
imports: [
MatTableModule,
MatSortModule,
MatInputModule,
ReactiveFormsModule,
MatSelectModule,
NoopAnimationsModule
]
});
fixture = TestBed.createComponent(CounterTable);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,142 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AfterViewInit, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { CounterEntity } from '../../../state/counter-listing';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { FormBuilder, FormGroup } from '@angular/forms';
import { debounceTime } from 'rxjs';
@Component({
selector: 'counter-table',
templateUrl: './counter-table.component.html',
styleUrls: ['./counter-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
})
export class CounterTable implements AfterViewInit {
private _canModifyCounters: boolean = false;
filterTerm: string = '';
filterColumn: 'context' | 'name' = 'name';
totalCount: number = 0;
filteredCount: number = 0;
displayedColumns: string[] = ['context', 'name', 'value'];
dataSource: MatTableDataSource<CounterEntity> = new MatTableDataSource<CounterEntity>();
filterForm: FormGroup;
@Input() initialSortColumn: 'context' | 'name' = 'context';
@Input() initialSortDirection: 'asc' | 'desc' = 'asc';
@Input() set counters(counterEntities: CounterEntity[]) {
this.dataSource = new MatTableDataSource<CounterEntity>(counterEntities);
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (data: CounterEntity, displayColumn: string) => {
switch (displayColumn) {
case 'context':
return this.formatContext(data);
case 'name':
return this.formatName(data);
case 'value':
return data.valueCount;
default:
return '';
}
};
this.dataSource.filterPredicate = (data: CounterEntity, filter: string) => {
const filterArray = filter.split('|');
const filterTerm = filterArray[0];
const filterColumn = filterArray[1];
if (filterColumn === 'name') {
return data.name.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
} else {
return data.context.toLowerCase().indexOf(filterTerm.toLowerCase()) >= 0;
}
};
this.totalCount = counterEntities.length;
this.filteredCount = counterEntities.length;
// apply any filtering to the new data
const filterTerm = this.filterForm.get('filterTerm')?.value;
if (filterTerm?.length > 0) {
const filterColumn = this.filterForm.get('filterColumn')?.value;
this.applyFilter(filterTerm, filterColumn);
}
}
@Input()
set canModifyCounters(canWrite: boolean) {
if (canWrite) {
this.displayedColumns = ['context', 'name', 'value', 'reset'];
} else {
this.displayedColumns = ['context', 'name', 'value'];
}
this._canModifyCounters = canWrite;
}
get canModifyCounters(): boolean {
return this._canModifyCounters;
}
@Output() resetCounter: EventEmitter<CounterEntity> = new EventEmitter<CounterEntity>();
@ViewChild(MatSort) sort!: MatSort;
constructor(private formBuilder: FormBuilder) {
this.filterForm = this.formBuilder.group({ filterTerm: '', filterColumn: 'name' });
}
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.filterForm
.get('filterTerm')
?.valueChanges.pipe(debounceTime(500))
.subscribe((filterTerm: string) => {
const filterColumn = this.filterForm.get('filterColumn')?.value;
this.applyFilter(filterTerm, filterColumn);
});
this.filterForm.get('filterColumn')?.valueChanges.subscribe((filterColumn: string) => {
const filterTerm = this.filterForm.get('filterTerm')?.value;
this.applyFilter(filterTerm, filterColumn);
});
}
applyFilter(filterTerm: string, filterColumn: string) {
this.dataSource.filter = `${filterTerm}|${filterColumn}`;
this.filteredCount = this.dataSource.filteredData.length;
}
formatContext(counter: CounterEntity): string {
return counter.context;
}
formatName(counter: CounterEntity): string {
return counter.name;
}
formatValue(counter: CounterEntity): string {
return counter.value;
}
resetClicked(counter: CounterEntity, event: MouseEvent) {
event.stopPropagation();
this.resetCounter.next(counter);
}
}

View File

@ -16,7 +16,7 @@
-->
<div class="relative h-full border">
<div class="parameter-context-table absolute inset-0 overflow-y-auto">
<div class="listing-table absolute inset-0 overflow-y-auto">
<table mat-table [dataSource]="dataSource" matSort matSortDisableClear>
<!-- More Details Column -->
<ng-container matColumnDef="moreDetails">

View File

@ -14,63 +14,3 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.parameter-context-table {
table {
width: 100%;
td,
th {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
}
th {
background-color: #728e9b;
color: #fff;
}
tr:hover {
background-color: #dce3e6 !important;
}
.selected {
background-color: #ffef85 !important;
}
.even {
background-color: #f4f6f7;
}
.fa,
.icon {
color: #004849;
width: 10px;
height: 14px;
text-align: center;
}
.mat-column-moreDetails {
min-width: 30px;
}
.mat-column-actions {
min-width: 115px;
}
}
.mat-mdc-table .mdc-data-table__header-row {
height: 35px;
}
.mat-mdc-table .mdc-data-table__row {
height: 35px;
}
}
::ng-deep .mat-sort-header-arrow {
color: #fff;
}

View File

@ -24,7 +24,7 @@ import { ParameterContextEntity } from '../../../state/parameter-context-listing
@Component({
selector: 'parameter-context-table',
templateUrl: './parameter-context-table.component.html',
styleUrls: ['./parameter-context-table.component.scss']
styleUrls: ['./parameter-context-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
})
export class ParameterContextTable implements AfterViewInit {
@Input() set parameterContexts(parameterContextEntities: ParameterContextEntity[]) {

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<div class="parameter-table flex gap-x-3">
<div class="parameter-table listing-table flex gap-x-3">
<div class="flex flex-col gap-y-3" style="flex-grow: 3">
<div class="flex justify-end items-center">
<button class="nifi-button" type="button" (click)="newParameterClicked()">

View File

@ -17,51 +17,18 @@
@use '@angular/material' as mat;
.parameter-table {
.parameter-table.listing-table {
@include mat.table-density(-4);
min-width: 740px;
table {
width: 100%;
td,
th {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
cursor: default;
}
th {
background-color: #728e9b;
color: #fff;
}
tr:hover {
background-color: #dce3e6 !important;
}
.selected {
background-color: #ffef85 !important;
}
.even {
background-color: #f4f6f7;
}
.fa {
width: 10px;
height: 14px;
}
.mat-column-actions {
width: 75px;
}
}
}
::ng-deep .mat-sort-header-arrow {
color: #fff;
}

View File

@ -57,7 +57,7 @@ export interface ParameterItem {
NifiTooltipDirective,
ParameterReferences
],
styleUrls: ['./parameter-table.component.scss'],
styleUrls: ['./parameter-table.component.scss', '../../../../../../assets/styles/listing-table.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

View File

@ -16,7 +16,7 @@
-->
<div class="relative h-full border">
<div class="reporting-task-table absolute inset-0 overflow-y-auto">
<div class="reporting-task-table listing-table absolute inset-0 overflow-y-auto">
<table mat-table [dataSource]="dataSource" matSort matSortDisableClear>
<!-- More Details Column -->
<ng-container matColumnDef="moreDetails">

View File

@ -15,62 +15,10 @@
* limitations under the License.
*/
.reporting-task-table {
.reporting-task-table.listing-table {
table {
width: 100%;
td,
th {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
}
th {
background-color: #728e9b;
color: #fff;
}
tr:hover {
background-color: #dce3e6 !important;
}
.selected {
background-color: #ffef85 !important;
}
.even {
background-color: #f4f6f7;
}
.fa,
.icon {
color: #004849;
width: 10px;
height: 14px;
text-align: center;
}
.mat-column-moreDetails {
min-width: 100px;
}
.mat-column-actions {
min-width: 115px;
}
}
.mat-mdc-table .mdc-data-table__header-row {
height: 35px;
}
.mat-mdc-table .mdc-data-table__row {
height: 35px;
}
}
::ng-deep .mat-sort-header-arrow {
color: #fff;
}

View File

@ -28,7 +28,7 @@ import { BulletinsTipInput, TextTipInput, ValidationErrorsTipInput } from '../..
@Component({
selector: 'reporting-task-table',
templateUrl: './reporting-task-table.component.html',
styleUrls: ['./reporting-task-table.component.scss']
styleUrls: ['./reporting-task-table.component.scss', '../../../../../../assets/styles/listing-table.scss']
})
export class ReportingTaskTable implements AfterViewInit {
@Input() set reportingTasks(reportingTaskEntities: ReportingTaskEntity[]) {

View File

@ -16,7 +16,7 @@
-->
<div class="relative h-full border">
<div class="controller-service-table absolute inset-0 overflow-y-auto">
<div class="controller-service-table listing-table absolute inset-0 overflow-y-auto">
<table mat-table [dataSource]="dataSource" matSort matSortDisableClear>
<!-- More Details Column -->
<ng-container matColumnDef="moreDetails">

View File

@ -17,56 +17,12 @@
@use '@angular/material' as mat;
.controller-service-table {
.controller-service-table.listing-table {
@include mat.table-density(-4);
table {
width: 100%;
td,
th {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
}
th {
background-color: #728e9b;
color: #fff;
}
tr:hover {
background-color: #dce3e6 !important;
}
.selected {
background-color: #ffef85 !important;
}
.even {
background-color: #f4f6f7;
}
.fa,
.icon {
color: #004849;
width: 10px;
height: 14px;
text-align: center;
}
.mat-column-moreDetails {
min-width: 90px;
}
.mat-column-actions {
min-width: 115px;
}
}
}
::ng-deep .mat-sort-header-arrow {
color: #fff;
}

View File

@ -38,7 +38,7 @@ import { ValidationErrorsTip } from '../../tooltips/validation-errors-tip/valida
standalone: true,
templateUrl: './controller-service-table.component.html',
imports: [MatButtonModule, MatDialogModule, MatTableModule, MatSortModule, NgIf, NgClass, NifiTooltipDirective],
styleUrls: ['./controller-service-table.component.scss']
styleUrls: ['./controller-service-table.component.scss', '../../../../../assets/styles/listing-table.scss']
})
export class ControllerServiceTable implements AfterViewInit {
@Input() set controllerServices(controllerServiceEntities: ControllerServiceEntity[]) {

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<div class="property-table flex flex-col gap-y-3">
<div class="property-table listing-table flex flex-col gap-y-3">
<div class="flex justify-between items-center">
<div class="font-bold">Required field</div>
<div>

View File

@ -17,7 +17,7 @@
@use '@angular/material' as mat;
.property-table {
.property-table.listing-table {
@include mat.table-density(-4);
min-width: 740px;
@ -26,46 +26,11 @@
td,
th {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
cursor: default;
}
th {
background-color: #728e9b;
color: #fff;
}
tr:hover {
background-color: #dce3e6 !important;
}
.selected {
background-color: #ffef85 !important;
}
.even {
background-color: #f4f6f7;
}
.fa {
width: 10px;
height: 14px;
}
.mat-column-actions {
min-width: 115px;
}
.mat-column-property {
min-width: 230px;
}
}
}
::ng-deep .mat-sort-header-arrow {
color: #fff;
}

View File

@ -87,7 +87,7 @@ export interface PropertyItem extends Property {
RouterLink,
AsyncPipe
],
styleUrls: ['./property-table.component.scss'],
styleUrls: ['./property-table.component.scss', '../../../../assets/styles/listing-table.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,

View File

@ -0,0 +1,76 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.listing-table {
table {
width: 100%;
td,
th {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 8px;
}
th {
background-color: #728e9b;
color: #fff;
}
tr:hover {
background-color: #dce3e6 !important;
}
.selected {
background-color: #ffef85 !important;
}
.even {
background-color: #f4f6f7;
}
.fa,
.icon {
color: #004849;
width: 10px;
height: 14px;
text-align: center;
}
.mat-column-moreDetails {
min-width: 30px;
}
.mat-column-actions {
min-width: 115px;
}
}
.mat-mdc-table .mdc-data-table__header-row {
height: 35px;
}
.mat-mdc-table .mdc-data-table__row {
height: 35px;
}
}
::ng-deep .mat-sort-header-arrow {
color: #fff;
}