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..8b49fd6da1 100644 --- a/ui-ngx/src/app/core/http/audit-log.service.ts +++ b/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> { - return this.http.get>(`/api/audit/logs${pageLink.toQuery()}`, + return this.http.get>(`/api/audit/logs${auditLogQuery.toQuery()}`, defaultHttpOptionsFromConfig(config)); } - public getAuditLogsByCustomerId(customerId: string, pageLink: TimePageLink, + public getAuditLogsByCustomerId(auditLogQuery: AuditLogQuery, config?: RequestConfig): Observable> { - return this.http.get>(`/api/audit/logs/customer/${customerId}${pageLink.toQuery()}`, + return this.http.get>(`/api/audit/logs/customer/${auditLogQuery.toQuery()}`, defaultHttpOptionsFromConfig(config)); } - public getAuditLogsByUserId(userId: string, pageLink: TimePageLink, + public getAuditLogsByUserId(auditLogQuery: AuditLogQuery, config?: RequestConfig): Observable> { - return this.http.get>(`/api/audit/logs/user/${userId}${pageLink.toQuery()}`, + return this.http.get>(`/api/audit/logs/user/${auditLogQuery.toQuery()}`, defaultHttpOptionsFromConfig(config)); } - public getAuditLogsByEntityId(entityId: EntityId, pageLink: TimePageLink, + public getAuditLogsByEntityId(auditLogQuery: AuditLogQuery, config?: RequestConfig): Observable> { - return this.http.get>(`/api/audit/logs/entity/${entityId.entityType}/${entityId.id}${pageLink.toQuery()}`, + return this.http.get>(`/api/audit/logs/entity/${auditLogQuery.toQuery()}`, defaultHttpOptionsFromConfig(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..7e0a104ef4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html @@ -0,0 +1,61 @@ + + + + + + + + + + +
+ +
+ + +
+
+
+ +
+ + +
+
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..502899f838 --- /dev/null +++ b/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; + } + } +} 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..c2559fdfbc --- /dev/null +++ b/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('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; + + @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(); + } + } +} 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.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-header.component.ts new file mode 100644 index 0000000000..e596c74460 --- /dev/null +++ b/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 { + + constructor(protected store: Store) { + super(store) + } + + auditLogFiltersChanged(auditLogFilter: AuditLogFilter) { + console.log("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..e70ee5aec7 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,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 { @@ -71,6 +75,7 @@ export class AuditLogTableConfig extends EntityTableConfig; + 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> { + 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 { 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; + } +} 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 0c501f5c5e..fe3287f1ab 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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",