Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NAS-133031 / 25.04 / Update audit page to master-detail view #11205

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
NAS-133031: Update audit page to master-detail view
  • Loading branch information
denysbutenko committed Dec 20, 2024
commit 2dcc35c4a57206748329a01a4f1451f3ee965d83
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
selector: 'ix-master-detail-view',
exportAs: 'masterDetailViewContext',
template: '<ng-content></ng-content>',
})
export class MockMasterDetailViewComponent {
isMobileView = jest.fn(() => false);
showMobileDetails = jest.fn(() => false);
toggleShowMobileDetails = jest.fn();
}
4 changes: 1 addition & 3 deletions src/app/pages/audit/audit.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,13 @@
></ix-audit-list>

<ng-container detail-header>
{{ 'Details for' | translate }}
{{ dataProvider.expandedRow?.event }}
{{ 'Log Details' | translate }}
</ng-container>

<ng-container detail>
@if (dataProvider.expandedRow) {
<ix-log-details-panel
[log]="dataProvider.expandedRow"
(hide)="closeMobileDetails()"
></ix-log-details-panel>
}
</ng-container>
Expand Down
25 changes: 0 additions & 25 deletions src/app/pages/audit/audit.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@
height: 100%;
}

ix-table {
height: 100%;
}

.user-avatar,
.user-avatar>div {
align-items: center;
Expand All @@ -32,27 +28,6 @@
}
}

.sticky-header {
height: inherit;
min-height: 48px;
top: 38px;

@media(max-width: $breakpoint-md) {
position: static;
}

>thead {
align-items: center;
display: flex;
width: 100%;

th {
align-items: center;
display: flex;
}
}
}

.table-container {
flex: 2;
margin-right: 0;
Expand Down
61 changes: 6 additions & 55 deletions src/app/pages/audit/audit.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonToggleChange, MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { provideMockStore } from '@ngrx/store/testing';
import { MockComponents } from 'ng-mocks';
import { mockCall, mockApi } from 'app/core/testing/utils/mock-api.utils';
import { AuditEvent, AuditService } from 'app/enums/audit.enum';
import { ControllerType } from 'app/enums/controller-type.enum';
import { AdvancedConfig } from 'app/interfaces/advanced-config.interface';
import { AuditEntry } from 'app/interfaces/audit/audit.interface';
import { ExportButtonComponent } from 'app/modules/buttons/export-button/export-button.component';
import { SearchInputComponent } from 'app/modules/forms/search-input/components/search-input/search-input.component';
import { IxTableHarness } from 'app/modules/ix-table/components/ix-table/ix-table.harness';
import { IxTableCellDirective } from 'app/modules/ix-table/directives/ix-table-cell.directive';
import { FakeProgressBarComponent } from 'app/modules/loader/components/fake-progress-bar/fake-progress-bar.component';
import { MockMasterDetailViewComponent } from 'app/modules/master-detail-view/testing/mock-master-detail-view.component';
import { PageHeaderComponent } from 'app/modules/page-header/page-title-header/page-header.component';
import { AuditComponent } from 'app/pages/audit/audit.component';
import { LogDetailsPanelComponent } from 'app/pages/audit/components/log-details-panel/log-details-panel.component';
Expand Down Expand Up @@ -72,6 +71,7 @@ describe('AuditComponent', () => {
ExportButtonComponent,
FakeProgressBarComponent,
PageHeaderComponent,
MockMasterDetailViewComponent,
),
],
providers: [
Expand Down Expand Up @@ -132,58 +132,9 @@ describe('AuditComponent', () => {
]);
});

it('searches by event, username and service when basic search is used', () => {
const search = spectator.query(SearchInputComponent);
search.query.set({
isBasicQuery: true,
query: 'search',
});

search.runSearch.emit();

expect(api.call).toHaveBeenLastCalledWith(
'audit.query',
[{
'query-filters': [['OR', [['event', '~', '(?i)search'], ['username', '~', '(?i)search'], ['service', '~', '(?i)search']]]],
'query-options': { limit: 50, offset: 0, order_by: ['-message_timestamp'] },
remote_controller: false,
}],
);
});

it('runs search when controller type is changed', () => {
spectator.component.controllerTypeChanged({ value: ControllerType.Standby } as MatButtonToggleChange);
spectator.detectChanges();

expect(api.call).toHaveBeenLastCalledWith(
'audit.query',
[{
'query-filters': [],
'query-options': { limit: 50, offset: 0, order_by: ['-message_timestamp'] },
remote_controller: true,
}],
);
});

it('applies filters to API query when advanced search is used', () => {
const search = spectator.query<SearchInputComponent<AuditEntry>>(SearchInputComponent);
search.query.set({
isBasicQuery: false,
filters: [
['event', '=', 'Authentication'],
['username', '~', 'bob'],
],
});
search.runSearch.emit();

expect(api.call).toHaveBeenLastCalledWith(
'audit.query',
[{
'query-filters': [['event', '=', 'Authentication'], ['username', '~', 'bob']],
'query-options': { limit: 50, offset: 0, order_by: ['-message_timestamp'] },
remote_controller: false,
}],
);
it('checks card title', () => {
const title = spectator.query('h3');
expect(title).toHaveText('Log Details');
});

it('shows details for the selected audit entry', async () => {
Expand Down
69 changes: 9 additions & 60 deletions src/app/pages/audit/audit.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BreakpointObserver, BreakpointState, Breakpoints } from '@angular/cdk/layout';
import {
ChangeDetectionStrategy, ChangeDetectorRef, Component, effect, Inject, OnDestroy, OnInit,
signal,
viewChild,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatAnchor } from '@angular/material/button';
Expand Down Expand Up @@ -60,74 +60,46 @@ export class AuditComponent implements OnInit, OnDestroy {
protected readonly searchableElements = auditElements;
protected readonly controllerType = signal<ControllerType>(ControllerType.Active);

protected dataProvider: AuditApiDataProvider;
protected dataProvider = new AuditApiDataProvider(this.api, {
paginationStrategy: new PaginationServerSide(),
sortingStrategy: new SortingServerSide(),
});

protected readonly isHaLicensed = toSignal(this.store$.select(selectIsHaLicensed));
protected readonly ControllerType = ControllerType;
protected readonly auditEventLabels = auditEventLabels;

showMobileDetails = false;
isMobileView = false;
masterDetailView = viewChild(MasterDetailViewComponent);
searchQuery: SearchQuery<AuditEntry>;
pagination: TablePagination;

constructor(
private api: ApiService,
protected emptyService: EmptyService,
private breakpointObserver: BreakpointObserver,
private cdr: ChangeDetectorRef,
private store$: Store<AppState>,
@Inject(WINDOW) private window: Window,
) {
effect(() => {
this.dataProvider.selectedControllerType = this.controllerType();
this.dataProvider.isHaLicensed = this.isHaLicensed();

this.dataProvider.load();
});
}

ngOnInit(): void {
this.dataProvider = new AuditApiDataProvider(this.api);
this.dataProvider.paginationStrategy = new PaginationServerSide();
this.dataProvider.sortingStrategy = new SortingServerSide();
this.setDefaultSort();

this.getAuditLogs();

this.dataProvider.load();
this.dataProvider.currentPage$.pipe(filter(Boolean), untilDestroyed(this)).subscribe((auditEntries) => {
this.dataProvider.expandedRow = this.isMobileView ? null : auditEntries[0];
this.expanded(this.dataProvider.expandedRow);
this.dataProvider.expandedRow = this.masterDetailView().isMobileView() ? null : auditEntries[0];
this.cdr.markForCheck();
});

this.initMobileView();
}

ngOnDestroy(): void {
this.dataProvider.unsubscribe();
}

closeMobileDetails(): void {
this.showMobileDetails = false;
this.dataProvider.expandedRow = null;
this.cdr.markForCheck();
}

expanded(row: AuditEntry): void {
if (!row) {
return;
}

if (this.isMobileView) {
this.showMobileDetails = true;
this.cdr.markForCheck();

// TODO: Do not rely on querying DOM elements
// focus on details container
setTimeout(() => (this.window.document.getElementsByClassName('mobile-back-button')[0] as HTMLElement).focus(), 0);
}
}

controllerTypeChanged(changedValue: MatButtonToggleChange): void {
if (this.controllerType() === changedValue.value) {
return;
Expand All @@ -140,34 +112,11 @@ export class AuditComponent implements OnInit, OnDestroy {
}
}

private initMobileView(): void {
this.breakpointObserver
.observe([Breakpoints.XSmall, Breakpoints.Small, Breakpoints.Medium])
.pipe(untilDestroyed(this))
.subscribe((state: BreakpointState) => {
if (state.matches) {
this.isMobileView = true;
if (this.dataProvider.expandedRow) {
this.expanded(this.dataProvider.expandedRow);
} else {
this.closeMobileDetails();
}
} else {
this.isMobileView = false;
}
this.cdr.markForCheck();
});
}

private setDefaultSort(): void {
this.dataProvider.setSorting({
propertyName: 'message_timestamp',
direction: SortDirection.Desc,
active: 1,
});
}

private getAuditLogs(): void {
this.dataProvider.load();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@import 'scss-imports/variables';
@import 'mixins/layout';

:host ::ng-deep .sticky-header {
height: inherit;
min-height: 48px;
top: 38px;

@media(max-width: $breakpoint-md) {
position: static;
}

>thead {
align-items: center;
display: flex;
width: 100%;

th {
align-items: center;
display: flex;
}
}
}
Loading
Loading