Browse Source

added filters to audit logs

pull/14262/head
Maksym Tsymbarov 10 months ago
parent
commit
f2f40b17ca
  1. 18
      ui-ngx/src/app/core/http/audit-log.service.ts
  2. 61
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html
  3. 36
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss
  4. 226
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts
  5. 20
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html
  6. 39
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts
  7. 25
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts
  8. 3
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts
  9. 2
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  10. 4
      ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts
  11. 38
      ui-ngx/src/app/shared/models/audit-log.models.ts
  12. 7
      ui-ngx/src/assets/locale/locale.constant-en_US.json

18
ui-ngx/src/app/core/http/audit-log.service.ts

@ -20,7 +20,7 @@ import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { TimePageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { AuditLog } from '@shared/models/audit-log.models';
import { AuditLog, AuditLogQuery } from '@shared/models/audit-log.models';
import { EntityId } from '@shared/models/id/entity-id';
@Injectable({
@ -32,27 +32,27 @@ export class AuditLogService {
private http: HttpClient
) { }
public getAuditLogs(pageLink: TimePageLink,
public getAuditLogs(auditLogQuery: AuditLogQuery,
config?: RequestConfig): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(`/api/audit/logs${pageLink.toQuery()}`,
return this.http.get<PageData<AuditLog>>(`/api/audit/logs${auditLogQuery.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink,
public getAuditLogsByCustomerId(auditLogQuery: AuditLogQuery,
config?: RequestConfig): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/customer/${customerId}${pageLink.toQuery()}`,
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/customer/${auditLogQuery.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getAuditLogsByUserId(userId: string, pageLink: TimePageLink,
public getAuditLogsByUserId(auditLogQuery: AuditLogQuery,
config?: RequestConfig): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/user/${userId}${pageLink.toQuery()}`,
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/user/${auditLogQuery.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink,
public getAuditLogsByEntityId(auditLogQuery: AuditLogQuery,
config?: RequestConfig): Observable<PageData<AuditLog>> {
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/entity/${entityId.entityType}/${entityId.id}${pageLink.toQuery()}`,
return this.http.get<PageData<AuditLog>>(`/api/audit/logs/entity/${auditLogQuery.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}

61
ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html

@ -0,0 +1,61 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed 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="panelMode; else componentMode">
<ng-container *ngTemplateOutlet="auditLogFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else auditLogFilter">
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
mat-stroked-button
(click)="toggleAuditLogFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
<ng-template #auditLogFilterPanel>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="auditLogFilter"></ng-container>
<div class="tb-panel-actions flex flex-row items-center justify-end">
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="auditLogFilterForm.invalid || !auditLogFilterForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>
</ng-template>
<ng-template #auditLogFilter>
<div class="flex flex-col gap-4" style="min-width: 280px;" [formGroup]="auditLogFilterForm">
<tb-string-items-list [label]="'audit-log.type' | translate"
appearance="outline"
subscriptSizing="dynamic"
[placeholder]="'audit-log.filter-type-add' | translate"
[predefinedValues]="predefinedTypeValues"
formControlName="types"
>
</tb-string-items-list>
</div>
</ng-template>

36
ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed 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.
*/
:host {
display: block;
overflow: hidden;
max-width: 100%;
padding-right: 20px;
.mdc-button {
max-width: 100%;
}
}
:host ::ng-deep {
.mdc-button {
.mat-icon {
min-width: 24px;
}
.mdc-button__label {
overflow: hidden;
text-overflow: ellipsis;
}
}
}

226
ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts

@ -0,0 +1,226 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed 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 { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { ChangeDetectorRef, Component, DestroyRef, ElementRef, forwardRef, Inject, InjectionToken, Input, OnInit, Optional, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { isDefinedAndNotNull } from '@app/core/utils';
import { StringItemsOption } from '@app/shared/components/string-items-list.component';
import { ActionType, actionTypeTranslations, AuditLogFilter, coerceBoolean } from '@app/shared/public-api';
import { TranslateService } from '@ngx-translate/core';
export const AUDIT_LOG_FILTER_CONFIG_DATA = new InjectionToken<any>('AuditLogFilterConfigData');
export interface AuditLogConfigData {
panelMode: boolean;
auditLogFilter: AuditLogFilter;
}
@Component({
selector: 'tb-audit-log-filter',
templateUrl: './audit-log-filter.component.html',
styleUrl: './audit-log-filter.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AuditLogFilterComponent),
multi: true
}
]
})
export class AuditLogFilterComponent implements OnInit, ControlValueAccessor {
@ViewChild('auditLogFilterPanel')
auditLogFilterPanel: TemplateRef<any>;
@Input() disabled: boolean;
@coerceBoolean()
@Input()
buttonMode = true;
ActionType = ActionType;
actionTypes = Object.values(ActionType);
actionTypeTranslations = actionTypeTranslations;
panelMode = false;
buttonDisplayValue = this.translate.instant('audit-log.filter');
auditLogFilterForm: UntypedFormGroup;
auditLogOverlayRef: OverlayRef;
panelResult: AuditLogFilter = null;
private auditLogFilter: AuditLogFilter;
private propagateChange = (_: any) => {};
constructor(@Optional() @Inject(AUDIT_LOG_FILTER_CONFIG_DATA)
private data: AuditLogConfigData | undefined,
@Optional()
private overlayRef: OverlayRef,
private fb: UntypedFormBuilder,
private translate: TranslateService,
private overlay: Overlay,
private nativeElement: ElementRef,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private destroyRef: DestroyRef) {}
ngOnInit(): void {
if (this.data) {
this.panelMode = this.data.panelMode;
this.auditLogFilter = this.data.auditLogFilter;
}
this.auditLogFilterForm = this.fb.group({
types: ['', []]
});
this.auditLogFilterForm.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => {
this.updateValidators();
if (!this.buttonMode) {
this.auditLogFilterUpdated(this.auditLogFilterForm.value);
}
}
);
if (this.panelMode) {
this.updateAuditLogFilterForm(this.auditLogFilter);
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.auditLogFilterForm.disable({emitEvent: false});
} else {
this.auditLogFilterForm.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(auditLogFilter?: AuditLogFilter): void {
this.auditLogFilter = auditLogFilter;
this.updateButtonDisplayValue();
this.updateAuditLogFilterForm(auditLogFilter);
}
get predefinedTypeValues (): StringItemsOption[] {
return [...this.actionTypes].map(type => ({
name: this.translate.instant(this.actionTypeTranslations.get(type)),
value: type
}))
}
private updateValidators() {
}
toggleAuditLogFilterPanel($event: Event) {
if ($event) {
$event.stopPropagation();
}
const config = new OverlayConfig({
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxHeight: '80vh',
height: 'min-content',
minWidth: ''
});
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement)
.withPositions([connectedPosition]);
this.auditLogOverlayRef = this.overlay.create(config);
this.auditLogOverlayRef.backdropClick().subscribe(() => {
this.auditLogOverlayRef.dispose();
});
this.auditLogOverlayRef.attach(new TemplatePortal(this.auditLogFilterPanel,
this.viewContainerRef));
}
cancel() {
this.updateAuditLogFilterForm(this.auditLogFilter);
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.auditLogOverlayRef.dispose();
}
}
update() {
this.auditLogFilterUpdated(this.auditLogFilterForm.value);
if (this.panelMode) {
this.panelResult = this.auditLogFilter;
}
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.auditLogOverlayRef.dispose();
}
}
private updateAuditLogFilterForm(auditLogFilter?: AuditLogFilter) {
this.auditLogFilterForm.patchValue({
types: auditLogFilter?.types,
}, {emitEvent: false});
this.updateValidators();
}
private auditLogFilterUpdated(auditLogFilter: AuditLogFilter) {
this.auditLogFilter = auditLogFilter;
this.updateButtonDisplayValue();
this.propagateChange(this.auditLogFilter);
}
private updateButtonDisplayValue() {
if (this.buttonMode) {
const filterTextParts: string[] = [];
if (isDefinedAndNotNull(this.auditLogFilter?.types)) {
const types = this.auditLogFilter.types;
types.forEach(type => filterTextParts.push(this.translate.instant(this.actionTypeTranslations.get(type as ActionType))))
}
if (!filterTextParts.length) {
this.buttonDisplayValue = this.translate.instant('audit-log.audit-log-filter-title');
} else {
this.buttonDisplayValue = this.translate.instant('audit-log.filter-title') + `: ${filterTextParts.join(', ')}`;
}
this.cd.detectChanges();
}
}
}

20
ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html

@ -0,0 +1,20 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed 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.
-->
<tb-audit-log-filter [ngModel]="entitiesTableConfig.componentsData.auditLogFilter"
(ngModelChange)="auditLogFiltersChanged($event)"
></tb-audit-log-filter>

39
ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts

@ -0,0 +1,39 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { AppState } from '@app/core/public-api';
import { AuditLog, AuditLogFilter, TimePageLink } from '@app/shared/public-api';
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component';
import { Store } from '@ngrx/store';
@Component({
selector: 'tb-audit-log-header',
templateUrl: './audit-log-header.component.html',
styles: ``
})
export class AuditLogHeaderComponent extends EntityTableHeaderComponent<AuditLog, TimePageLink> {
constructor(protected store: Store<AppState>) {
super(store)
}
auditLogFiltersChanged(auditLogFilter: AuditLogFilter) {
console.log("auditLogFilter",auditLogFilter )
this.entitiesTableConfig.componentsData.auditLogFilter = auditLogFilter;
this.entitiesTableConfig.getTable().resetSortAndFilter(true, true);
}
}

25
ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts

@ -25,7 +25,9 @@ import {
ActionType,
actionTypeTranslations,
AuditLog,
AuditLogMode
AuditLogMode,
AuditLogQuery,
AuditLogRequestParams
} from '@shared/models/audit-log.models';
import {
AliasEntityType,
@ -48,6 +50,8 @@ import {
AuditLogDetailsDialogComponent,
AuditLogDetailsDialogData
} from '@home/components/audit-log/audit-log-details-dialog.component';
import { deepClone, isDefinedAndNotNull } from '@app/core/utils';
import { AuditLogHeaderComponent } from '@home/components/audit-log/audit-log-header.component';
export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLink> {
@ -71,6 +75,7 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
this.searchEnabled = true;
this.addEnabled = false;
this.entitiesDeleteEnabled = false;
this.headerComponent = AuditLogHeaderComponent;
this.actionsColumnTitle = 'audit-log.details';
this.entityTranslations = {
noEntities: 'audit-log.no-audit-logs-prompt',
@ -79,6 +84,8 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
this.entityResources = {
} as EntityTypeResource<AuditLog>;
if(!isDefinedAndNotNull(this.componentsData)) this.componentsData = {}
this.entitiesFetchFunction = pageLink => this.fetchAuditLogs(pageLink);
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
@ -139,15 +146,19 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
}
fetchAuditLogs(pageLink: TimePageLink): Observable<PageData<AuditLog>> {
const auditLogFilter: AuditLogRequestParams = deepClone(this.componentsData?.auditLogFilter) || {};
switch (this.auditLogMode) {
case AuditLogMode.TENANT:
return this.auditLogService.getAuditLogs(pageLink);
return this.auditLogService.getAuditLogs(this.prepareAuditLogQuery(pageLink, auditLogFilter));
case AuditLogMode.ENTITY:
return this.auditLogService.getAuditLogsByEntityId(this.entityId, pageLink);
auditLogFilter.id = `${this.entityId.entityType}/${this.entityId.id}`;
return this.auditLogService.getAuditLogsByEntityId(this.prepareAuditLogQuery(pageLink, auditLogFilter));
case AuditLogMode.USER:
return this.auditLogService.getAuditLogsByUserId(this.userId.id, pageLink);
auditLogFilter.id = this.userId.id;
return this.auditLogService.getAuditLogsByUserId(this.prepareAuditLogQuery(pageLink, auditLogFilter));
case AuditLogMode.CUSTOMER:
return this.auditLogService.getAuditLogsByCustomerId(this.customerId.id, pageLink);
auditLogFilter.id = this.customerId.id;
return this.auditLogService.getAuditLogsByCustomerId(this.prepareAuditLogQuery(pageLink, auditLogFilter));
}
}
@ -161,4 +172,8 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
});
}
prepareAuditLogQuery(pageLink: TimePageLink, auditLogFilter: AuditLogRequestParams): AuditLogQuery {
return new AuditLogQuery(pageLink, auditLogFilter);
}
}

3
ui-ngx/src/app/modules/home/components/audit-log/audit-log-table.component.ts

@ -97,6 +97,9 @@ export class AuditLogTableComponent implements OnInit {
}
}
@Input()
isFilterShown: boolean;
@ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent;
auditLogTableConfig: AuditLogTableConfig;

2
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -23,6 +23,7 @@ import { DetailsPanelComponent } from '@home/components/details-panel.component'
import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component';
import { AuditLogDetailsDialogComponent } from '@home/components/audit-log/audit-log-details-dialog.component';
import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component';
import { AuditLogFilterComponent } from '@home/components/audit-log/audit-log-filter.component';
import { EventTableHeaderComponent } from '@home/components/event/event-table-header.component';
import { EventTableComponent } from '@home/components/event/event-table.component';
import { EventFilterPanelComponent } from '@home/components/event/event-filter-panel.component';
@ -208,6 +209,7 @@ import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rul
EntityDetailsPanelComponent,
EntityDetailsPageComponent,
AuditLogTableComponent,
AuditLogFilterComponent,
AuditLogDetailsDialogComponent,
CalculatedFieldsTableComponent,
CalculatedFieldDebugDialogComponent,

4
ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts

@ -18,13 +18,17 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { AuditLogRoutingModule } from '@modules/home/pages/audit-log/audit-log-routing.module';
import { AuditLogHeaderComponent } from '@home/components/audit-log/audit-log-header.component';
import { HomeComponentsModule } from '@home/components/public-api';
@NgModule({
declarations: [
AuditLogHeaderComponent,
],
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
AuditLogRoutingModule
]
})

38
ui-ngx/src/app/shared/models/audit-log.models.ts

@ -20,6 +20,9 @@ import { CustomerId } from './id/customer-id';
import { EntityId } from './id/entity-id';
import { UserId } from './id/user-id';
import { TenantId } from './id/tenant-id';
import { PageLink } from '@shared/models/page/page-link';
import { isDefinedAndNotNull } from '@app/core/utils';
import { StringItemsOption } from '../components/string-items-list.component';
export enum AuditLogMode {
TENANT,
@ -132,3 +135,38 @@ export interface AuditLog extends BaseData<AuditLogId> {
actionStatus: ActionStatus;
actionFailureDetails: string;
}
export interface AuditLogFilter {
types?: string[];
}
export interface AuditLogRequestParams {
types?: string[];
id?: string;
}
export class AuditLogQuery {
pageLink: PageLink;
auditLogFilter: AuditLogRequestParams;
constructor(pageLink: PageLink, auditLogFilter: AuditLogRequestParams) {
this.pageLink = pageLink;
this.auditLogFilter = auditLogFilter;
}
public toQuery(): string {
let query = '';
if(isDefinedAndNotNull(this.auditLogFilter.id)) {
query += `${this.auditLogFilter.id}`;
}
query += this.pageLink.toQuery();
if (isDefinedAndNotNull(this.auditLogFilter?.types)) {
const types = this.auditLogFilter.types.join(',');
query += `&actionTypes=${types}`;
}
return query;
}
}

7
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1033,7 +1033,12 @@
"type-provision-failure": "Device provisioning was failed",
"type-timeseries-updated": "Telemetry updated",
"type-timeseries-deleted": "Telemetry deleted",
"type-sms-sent": "SMS sent"
"type-sms-sent": "SMS sent",
"any":"Any",
"filter":"Audit Log Filters",
"audit-log-filter-title": "Audit Log Filter",
"filter-title": "Filter",
"filter-type-add": "+ Add type"
},
"debug-settings": {
"label": "Debug Configuration",

Loading…
Cancel
Save