diff --git a/ui-ngx/src/app/core/http/audit-log.service.ts b/ui-ngx/src/app/core/http/audit-log.service.ts index 3917930731..f45819db15 100644 --- a/ui-ngx/src/app/core/http/audit-log.service.ts +++ b/ui-ngx/src/app/core/http/audit-log.service.ts @@ -15,12 +15,12 @@ /// import { Injectable } from '@angular/core'; -import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { createDefaultHttpOptions, RequestConfig } from './http-utils'; 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, AuditLogFilter } from '@shared/models/audit-log.models'; import { EntityId } from '@shared/models/id/entity-id'; @Injectable({ @@ -32,28 +32,58 @@ export class AuditLogService { private http: HttpClient ) { } - public getAuditLogs(pageLink: TimePageLink, - config?: RequestConfig): Observable> { - return this.http.get>(`/api/audit/logs${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getAuditLogs(pageLink: TimePageLink, config?: RequestConfig): Observable>; + public getAuditLogs(pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable>; + public getAuditLogs( + pageLink: TimePageLink, + filtersOrConfig?: AuditLogFilter | RequestConfig, + config?: RequestConfig + ): Observable> { + return this.http.get>( + `/api/audit/logs${pageLink.toQuery()}`, + createDefaultHttpOptions(filtersOrConfig, config) + ); } - public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, - config?: RequestConfig): Observable> { - return this.http.get>(`/api/audit/logs/customer/${customerId}${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, config?: RequestConfig): Observable>; + public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable>; + public getAuditLogsByCustomerId( + customerId: string, + pageLink: TimePageLink, + filtersOrConfig?: AuditLogFilter | RequestConfig, + config?: RequestConfig + ): Observable> { + return this.http.get>( + `/api/audit/logs/customer/${customerId}${pageLink.toQuery()}`, + createDefaultHttpOptions(filtersOrConfig, config) + ); } - public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, - config?: RequestConfig): Observable> { - return this.http.get>(`/api/audit/logs/user/${userId}${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, config?: RequestConfig): Observable>; + public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable>; + public getAuditLogsByUserId( + userId: string, + pageLink: TimePageLink, + filtersOrConfig?: AuditLogFilter | RequestConfig, + config?: RequestConfig + ): Observable> { + return this.http.get>( + `/api/audit/logs/user/${userId}${pageLink.toQuery()}`, + createDefaultHttpOptions(filtersOrConfig, config) + ); } - public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, - config?: RequestConfig): Observable> { - return this.http.get>(`/api/audit/logs/entity/${entityId.entityType}/${entityId.id}${pageLink.toQuery()}`, - defaultHttpOptionsFromConfig(config)); + public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, config?: RequestConfig): Observable>; + public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, filters: AuditLogFilter, config?: RequestConfig): Observable>; + public getAuditLogsByEntityId( + entityId: EntityId, + pageLink: TimePageLink, + filtersOrConfig?: AuditLogFilter | RequestConfig, + config?: RequestConfig + ): Observable> { + return this.http.get>( + `/api/audit/logs/entity/${entityId.entityType}/${entityId.id}${pageLink.toQuery()}`, + createDefaultHttpOptions(filtersOrConfig, config) + ); } - } diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html new file mode 100644 index 0000000000..b97285ba07 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html @@ -0,0 +1,61 @@ + + + + +
+
+
+
audit-log.filter-types
+ + +
+
+
+ + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss new file mode 100644 index 0000000000..77bd5aea60 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss @@ -0,0 +1,35 @@ +/** + * 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: flex; + 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; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts new file mode 100644 index 0000000000..41d6838120 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts @@ -0,0 +1,198 @@ +/// +/// 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 { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { ChangeDetectorRef, Component, DestroyRef, ElementRef, forwardRef, Input, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { isDefined, isDefinedAndNotNull } from '@app/core/utils'; +import { StringItemsOption } from '@app/shared/components/string-items-list.component'; +import { + ActionType, + actionTypeTranslations, + auditLogFilterEquals, + AuditLogFilter, + POSITION_MAP, +} from '@app/shared/public-api'; +import { TranslateService } from '@ngx-translate/core'; + +// @dynamic +@Component({ + selector: 'tb-audit-log-filter', + templateUrl: './audit-log-filter.component.html', + styleUrls: ['./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; + + @Input() disabled: boolean; + + ActionType = ActionType; + + actionTypes = Object.values(ActionType); + + actionTypeTranslations = actionTypeTranslations; + + buttonDisplayValue = this.translate.instant('audit-log.filter'); + + auditLogFilterForm: UntypedFormGroup; + + auditLogOverlayRef: OverlayRef; + + initialAuditLogFilter: AuditLogFilter; + + private auditLogFilter: AuditLogFilter; + + private propagateChange = (_: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private translate: TranslateService, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private destroyRef: DestroyRef) {} + + ngOnInit(): void { + this.auditLogFilterForm = this.fb.group({ + actionTypes: [[]] + }); + this.auditLogFilterForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + this.updateValidators(); + } + ); + } + + 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; + if(!this.initialAuditLogFilter && isDefined(this.auditLogFilter)) { + this.initialAuditLogFilter = this.auditLogFilterForm.getRawValue(); + } + 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; + config.positionStrategy = this.overlay.position() + .flexibleConnectedTo(this.nativeElement) + .withPositions([POSITION_MAP.bottomLeft]); + + 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); + this.auditLogOverlayRef.dispose(); + } + + update() { + this.auditLogFilterUpdated(this.auditLogFilterForm.value); + this.auditLogOverlayRef.dispose(); + } + + reset() { + if (this.initialAuditLogFilter) { + if (!auditLogFilterEquals(this.auditLogFilter, this.initialAuditLogFilter)) { + this.updateAuditLogFilterForm(this.initialAuditLogFilter); + this.auditLogFilterForm.markAsDirty(); + } + } + } + + private updateAuditLogFilterForm(auditLogFilter?: AuditLogFilter) { + this.auditLogFilterForm.patchValue({ + actionTypes: auditLogFilter?.actionTypes, + }, {emitEvent: false}); + this.updateValidators(); + } + + private auditLogFilterUpdated(auditLogFilter: AuditLogFilter) { + this.auditLogFilter = auditLogFilter; + this.updateButtonDisplayValue(); + this.propagateChange(this.auditLogFilter); + } + + private updateButtonDisplayValue() { + const filterTextParts: string[] = []; + if (isDefinedAndNotNull(this.auditLogFilter?.actionTypes)) { + const actionTypes = this.auditLogFilter.actionTypes; + actionTypes.forEach(actionType => filterTextParts.push(this.translate.instant(this.actionTypeTranslations.get(actionType 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(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html new file mode 100644 index 0000000000..3d982197c8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.html @@ -0,0 +1,20 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.scss b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.scss new file mode 100644 index 0000000000..ee0899b5ca --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.scss @@ -0,0 +1,21 @@ +/** + * 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 { + padding-right: 8px; + overflow: hidden; + max-width: 100%; +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts new file mode 100644 index 0000000000..f3635415a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts @@ -0,0 +1,38 @@ +/// +/// 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 { + + constructor(protected store: Store) { + super(store) + } + + auditLogFiltersChanged(auditLogFilter: AuditLogFilter) { + this.entitiesTableConfig.componentsData.auditLogFilter = auditLogFilter; + this.entitiesTableConfig.getTable().resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts index 7055ace090..edad7b2cb7 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts @@ -25,7 +25,8 @@ import { ActionType, actionTypeTranslations, AuditLog, - AuditLogMode + AuditLogMode, + AuditLogFilter } from '@shared/models/audit-log.models'; import { AliasEntityType, @@ -48,6 +49,8 @@ import { AuditLogDetailsDialogComponent, AuditLogDetailsDialogData } from '@home/components/audit-log/audit-log-details-dialog.component'; +import { deepClone } from '@app/core/utils'; +import { AuditLogHeaderComponent } from '@home/components/audit-log/audit-log-header.component'; export class AuditLogTableConfig extends EntityTableConfig { @@ -71,6 +74,7 @@ export class AuditLogTableConfig extends EntityTableConfig; + this.componentsData = { + auditLogFilter: [] + }; + this.entitiesFetchFunction = pageLink => this.fetchAuditLogs(pageLink); this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; @@ -139,15 +147,16 @@ export class AuditLogTableConfig extends EntityTableConfig> { + const auditLogFilter: AuditLogFilter = deepClone(this.componentsData?.auditLogFilter) || {}; switch (this.auditLogMode) { case AuditLogMode.TENANT: - return this.auditLogService.getAuditLogs(pageLink); + return this.auditLogService.getAuditLogs(pageLink, auditLogFilter); case AuditLogMode.ENTITY: - return this.auditLogService.getAuditLogsByEntityId(this.entityId, pageLink); + return this.auditLogService.getAuditLogsByEntityId(this.entityId, pageLink, auditLogFilter); case AuditLogMode.USER: - return this.auditLogService.getAuditLogsByUserId(this.userId.id, pageLink); + return this.auditLogService.getAuditLogsByUserId(this.userId.id, pageLink, auditLogFilter); case AuditLogMode.CUSTOMER: - return this.auditLogService.getAuditLogsByCustomerId(this.customerId.id, pageLink); + return this.auditLogService.getAuditLogsByCustomerId(this.customerId.id, pageLink, auditLogFilter); } } diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts index b9617dfa7c..724aabf64c 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-table-header.component.ts @@ -17,8 +17,6 @@ import { BaseData, HasId } from '@shared/models/base-data'; import { PageComponent } from '@shared/components/page.component'; import { Directive, Input, OnInit } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { PageLink } from '@shared/models/page/page-link'; @@ -41,8 +39,8 @@ export abstract class EntityTableHeaderComponent, return this.entitiesTableConfigValue; } - protected constructor(protected store: Store) { - super(store); + protected constructor(...args: unknown[]) { + super(); } ngOnInit() { diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 2418eb9ad2..6da26fb2c3 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/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 { AuditLogHeaderComponent } from '@home/components/audit-log/audit-log-header.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'; @@ -202,6 +203,7 @@ import { AddApiKeyDialogComponent } from '@home/components/api-key/add-api-key-d import { EditApiKeyDescriptionPanelComponent } from '@home/components/api-key/edit-api-key-description-panel.component'; import { ApiKeyGeneratedDialogComponent } from '@home/components/api-key/api-key-generated-dialog.component'; import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-table-dialog.component'; +import { AuditLogFilterComponent } from "@home/components/audit-log/audit-log-filter.component"; @NgModule({ declarations: @@ -358,6 +360,8 @@ import { ApiKeysTableDialogComponent } from '@home/components/api-key/api-keys-t AddApiKeyDialogComponent, EditApiKeyDescriptionPanelComponent, ApiKeyGeneratedDialogComponent, + AuditLogHeaderComponent, + AuditLogFilterComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts index 59aa88b424..5f7c4b0626 100644 --- a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts +++ b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log.module.ts @@ -20,8 +20,7 @@ import { SharedModule } from '@shared/shared.module'; import { AuditLogRoutingModule } from '@modules/home/pages/audit-log/audit-log-routing.module'; @NgModule({ - declarations: [ - ], + declarations: [], imports: [ CommonModule, SharedModule, diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts index a77e50cdff..7a32bad041 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -20,6 +20,7 @@ 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 { isArraysEqualIgnoreUndefined } from "@core/utils"; export enum AuditLogMode { TENANT, @@ -132,3 +133,11 @@ export interface AuditLog extends BaseData { actionStatus: ActionStatus; actionFailureDetails: string; } + +export interface AuditLogFilter { + actionTypes: string[]; +} + +export const auditLogFilterEquals = (filter1?: AuditLogFilter, filter2?: AuditLogFilter): boolean => { + return isArraysEqualIgnoreUndefined(filter1.actionTypes, filter2.actionTypes); +}; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index c0623c49fc..538b3a8352 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1075,7 +1075,13 @@ "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", + "filter-types": "Audit Log Types" }, "debug-settings": { "label": "Debug Configuration",