Skip to content

Commit

Permalink
NAS-124972 / 24.04 / Add smb status locks table (#9175)
Browse files Browse the repository at this point in the history
* NAS-124972: SmbStatus: Implement Locks table and unit tests

* NAS-124972: Add smb status locks table

* NAS-124972: Add smb status locks table

* NAS-124972: Add smb status locks table

* NAS-124972: NAS-124970: Add smb status locks table

* NAS-124972: NAS-124970: Add smb status locks table

* NAS-124972: Add smb status locks table

* NAS-124972: Add smb status locks table

* NAS-124972: Add smb status locks table
  • Loading branch information
denysbutenko authored Dec 1, 2023
1 parent 6b9dbd8 commit 8d5f444
Show file tree
Hide file tree
Showing 102 changed files with 1,157 additions and 28 deletions.
56 changes: 55 additions & 1 deletion src/app/interfaces/smb-status.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type SmbStatus = SmbSession | SmbShareInfo | SmbNotificationInfo;
export type SmbStatus = SmbSession | SmbLockInfo | SmbShareInfo | SmbNotificationInfo;

interface SmbServerId {
pid: string;
Expand Down Expand Up @@ -34,6 +34,60 @@ export interface SmbSession {
share_connections: SmbShareConnection[];
}

export interface SmbLockInfo {
service_path: string;
filename: string;
fileid: {
devid: number;
inode: number;
extid: number;
};
num_pending_deletes: number;
opens: Record<string, SmbOpenInfo>;
}

export interface SmbOpenInfo {
server_id: SmbServerId;
uid: number;
username: string;
share_file_id: string;
sharemode: {
hex: string;
READ: boolean;
WRITE: boolean;
DELETE: boolean;
text: string;
};
access_mask: {
hex: string;
READ_DATA: boolean;
WRITE_DATA: boolean;
APPEND_DATA: boolean;
READ_EA: boolean;
WRITE_EA: boolean;
EXECUTE: boolean;
READ_ATTRIBUTES: boolean;
WRITE_ATTRIBUTES: boolean;
DELETE_CHILD: boolean;
DELETE: boolean;
READ_CONTROL: boolean;
WRITE_DAC: boolean;
SYNCHRONIZE: boolean;
ACCESS_SYSTEM_SECURITY: boolean;
text: string;
};
caching: {
READ: boolean;
WRITE: boolean;
HANDLE: boolean;
hex: string;
text: string;
};
oplock: Record<string, unknown>;
lease: Record<string, unknown>;
opened_at: string;
}

export interface SmbShareInfo {
service: string;
server_id: SmbServerId;
Expand Down
1 change: 1 addition & 0 deletions src/app/modules/ix-table2/classes/base-data-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class BaseDataProvider<T> implements DataProvider<T> {
}

currentPage$ = new BehaviorSubject<T[]>([]);
expandedRow$ = new BehaviorSubject<T>(null);
expandedRow: T;
totalRows = 0;

Expand Down
12 changes: 7 additions & 5 deletions src/app/pages/sharing/sharing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
Expand Down Expand Up @@ -43,8 +44,9 @@ import { TargetGlobalConfigurationComponent } from 'app/pages/sharing/iscsi/targ
import { NfsListComponent } from 'app/pages/sharing/nfs/nfs-list/nfs-list.component';
import { NfsSessionListComponent } from 'app/pages/sharing/nfs/nfs-session-list/nfs-session-list.component';
import { RestartSmbDialogComponent } from 'app/pages/sharing/smb/smb-form/restart-smb-dialog/restart-smb-dialog.component';
import { SmbLockListComponent } from 'app/pages/sharing/smb/smb-status/components/smb-lock-list/smb-lock-list.component';
import { SmbNotificationListComponent } from 'app/pages/sharing/smb/smb-status/components/smb-notification-list/smb-notification-list.component';
import { UserService } from 'app/services/user.service';
import { SmbShareListComponent } from 'app/pages/sharing/smb/smb-status/components/smb-share-list/smb-share-list.component';
import { ServiceExtraActionsComponent } from './components/shares-dashboard/service-extra-actions/service-extra-actions.component';
import { ServiceStateButtonComponent } from './components/shares-dashboard/service-state-button/service-state-button.component';
import { SharesDashboardComponent } from './components/shares-dashboard/shares-dashboard.component';
Expand All @@ -64,8 +66,8 @@ import { routing } from './sharing.routing';
import { SmbAclComponent } from './smb/smb-acl/smb-acl.component';
import { SmbFormComponent } from './smb/smb-form/smb-form.component';
import { SmbListComponent } from './smb/smb-list/smb-list.component';
import { SmbOpenFilesComponent } from './smb/smb-status/components/smb-open-files/smb-open-files.component';
import { SmbSessionListComponent } from './smb/smb-status/components/smb-session-list/smb-session-list.component';
import { SmbShareListComponent } from './smb/smb-status/components/smb-share-list/smb-share-list.component';
import { SmbStatusComponent } from './smb/smb-status/smb-status.component';

@NgModule({
Expand Down Expand Up @@ -100,6 +102,7 @@ import { SmbStatusComponent } from './smb/smb-status/smb-status.component';
MatToolbarModule,
CoreComponents,
LayoutModule,
MatExpansionModule,
MatButtonToggleModule,
],
declarations: [
Expand Down Expand Up @@ -135,14 +138,13 @@ import { SmbStatusComponent } from './smb/smb-status/smb-status.component';
ServiceExtraActionsComponent,
ServiceStateButtonComponent,
SmbSessionListComponent,
SmbLockListComponent,
SmbOpenFilesComponent,
NfsSessionListComponent,
SmbStatusComponent,
SmbShareListComponent,
SmbNotificationListComponent,
],
providers: [
UserService,
],
})
export class SharingModule {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<mat-card>
<mat-toolbar-row>
<h3>{{ 'Locks' | translate }}</h3>

<div class="actions action-icon">
<ix-search-input [value]="filterString" (search)="onListFiltered($event)"></ix-search-input>

<ix-table-columns-selector [columns]="columns" (columnsChange)="columnsChange($event)"></ix-table-columns-selector>

<button mat-button color="primary" ixTest="refresh-data" (click)="loadData()">
{{ 'Refresh' | translate }}
</button>
</div>
</mat-toolbar-row>
<mat-card-content>
<ix-table2
class="table"
[ix-table2-empty]="!(dataProvider.currentPageCount$ | async)"
[emptyConfig]="emptyService.defaultEmptyConfig(dataProvider.emptyType$ | async)"
>
<thead
ix-table-head
[columns]="columns"
[dataProvider]="dataProvider"
></thead>
<tbody
ix-table-body
[columns]="columns"
[dataProvider]="dataProvider"
[isLoading]="dataProvider.isLoading$ | async"
>
<ng-template let-lock ix-table-details-row [dataProvider]="dataProvider">
<ix-smb-open-files [lock]="lock"></ix-smb-open-files>
</ng-template>
</tbody>
</ix-table2>
<ix-table-pager [dataProvider]="dataProvider"></ix-table-pager>
</mat-card-content>
</mat-card>

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ix-table2::ng-deep .details td {
padding-bottom: 16px !important;
padding-top: 16px !important;

h3 {
margin-bottom: 8px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { Spectator } from '@ngneat/spectator';
import { createComponentFactory } from '@ngneat/spectator/jest';
import { mockWebsocket, mockCall } from 'app/core/testing/utils/mock-websocket.utils';
import { SmbLockInfo, SmbOpenInfo } from 'app/interfaces/smb-status.interface';
import { EntityModule } from 'app/modules/entity/entity.module';
import { IxTable2Harness } from 'app/modules/ix-table2/components/ix-table2/ix-table2.harness';
import { IxTable2Module } from 'app/modules/ix-table2/ix-table2.module';
import { AppLoaderModule } from 'app/modules/loader/app-loader.module';
import { SmbLockListComponent } from 'app/pages/sharing/smb/smb-status/components/smb-lock-list/smb-lock-list.component';

describe('SmbLockListComponent', () => {
let spectator: Spectator<SmbLockListComponent>;
let loader: HarnessLoader;
let table: IxTable2Harness;

const locks = [
{
service_path: '/mnt/APPS/turtles',
filename: '.',
fileid: { devid: 70, inode: 3, extid: 0 },
num_pending_deletes: 0,
opens: {
'2102401/69': {
server_id: {
pid: '2102401', task_id: '0', vnn: '4294967295', unique_id: '4458796888113407749',
},
uid: 3004,
username: 'michelangelo',
opened_at: '2023-10-26T12:17:27.190608+02:00',
} as SmbOpenInfo,
'2102401/70': {
server_id: {
pid: '2102401', task_id: '0', vnn: '4294967295', unique_id: '4458796888113407749',
},
uid: 3005,
username: 'donatello',
opened_at: '2023-10-26T12:18:27.190608+02:00',
} as SmbOpenInfo,
'2102401/71': {
server_id: {
pid: '2102401', task_id: '0', vnn: '4294967295', unique_id: '4458796888113407749',
},
uid: 3006,
username: 'raphael',
opened_at: '2023-10-26T10:10:10.190608+02:00',
} as SmbOpenInfo,
'2102401/72': {
server_id: {
pid: '2102401', task_id: '0', vnn: '4294967295', unique_id: '4458796888113407749',
},
uid: 3007,
username: 'leonardo',
opened_at: '2023-10-26T10:10:10.190608+02:00',
} as SmbOpenInfo,
},
},
] as SmbLockInfo[];

const createComponent = createComponentFactory({
component: SmbLockListComponent,
imports: [AppLoaderModule, EntityModule, IxTable2Module],
providers: [
mockWebsocket([
mockCall('smb.status', locks),
]),
],
});

beforeEach(async () => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
table = await loader.getHarness(IxTable2Harness);
});

it('should show table rows', async () => {
const expectedRows = [
[
'Path',
'Filename',
'File ID',
'Open Files',
'Num Pending Deletes',
],
[
'/mnt/APPS/turtles',
'.',
'70:3:0',
'4 open files',
'0',
],
];

const cells = await table.getCellTexts();
expect(cells).toEqual(expectedRows);
});

it('should call loadData when Refresh button is pressed', async () => {
jest.spyOn(spectator.component.dataProvider, 'load');
const refreshButton = await loader.getHarness(MatButtonHarness.with({ text: 'Refresh' }));
await refreshButton.click();
expect(spectator.component.dataProvider.load).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit,
} from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService } from '@ngx-translate/core';
import { tap } from 'rxjs';
import { SmbInfoLevel } from 'app/enums/smb-info-level.enum';
import { SmbLockInfo, SmbOpenInfo } from 'app/interfaces/smb-status.interface';
import { AsyncDataProvider } from 'app/modules/ix-table2/classes/async-data-provider/async-data-provider';
import { textColumn } from 'app/modules/ix-table2/components/ix-table-body/cells/ix-cell-text/ix-cell-text.component';
import { createTable } from 'app/modules/ix-table2/utils';
import { EmptyService } from 'app/modules/ix-tables/services/empty.service';
import { WebSocketService } from 'app/services/ws.service';

@UntilDestroy()
@Component({
selector: 'ix-smb-lock-list',
templateUrl: './smb-lock-list.component.html',
styleUrls: ['./smb-lock-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SmbLockListComponent implements OnInit {
filterString = '';
dataProvider: AsyncDataProvider<SmbLockInfo>;
locks: SmbLockInfo[] = [];
files: SmbOpenInfo[] = [];
columns = createTable<SmbLockInfo>([
textColumn({ title: this.translate.instant('Path'), propertyName: 'service_path' }),
textColumn({ title: this.translate.instant('Filename'), propertyName: 'filename' }),
textColumn({
title: this.translate.instant('File ID'),
propertyName: 'fileid',
getValue: (row) => {
return Object.values(row.fileid).join(':');
},
}),
textColumn({
title: this.translate.instant('Open Files'),
propertyName: 'opens',
getValue: (row) => {
return this.translate.instant('{n, plural, =0 {No open files} one {# open file} other {# open files}}', { n: Object.keys(row.opens).length });
},
}),
textColumn({
title: this.translate.instant('Num Pending Deletes'),
propertyName: 'num_pending_deletes',
}),
]);

constructor(
private ws: WebSocketService,
private translate: TranslateService,
private cdr: ChangeDetectorRef,
protected emptyService: EmptyService,
) {}

ngOnInit(): void {
const smbStatus$ = this.ws.call('smb.status', [SmbInfoLevel.Locks]).pipe(
tap((locks: SmbLockInfo[]) => {
this.locks = locks;
if (this.filterString) {
this.onListFiltered(this.filterString);
}
}),
untilDestroyed(this),
);

this.dataProvider = new AsyncDataProvider(smbStatus$);
this.loadData();
}

loadData(): void {
this.dataProvider.load();
}

onListFiltered(query: string): void {
this.filterString = query?.toString()?.toLowerCase();
this.dataProvider.setRows(this.locks.filter((lock) => {
return [
lock.filename,
lock.service_path,
].some((value) => value.toString().toLowerCase().includes(this.filterString));
}));
}

columnsChange(columns: typeof this.columns): void {
this.columns = [...columns];
this.cdr.detectChanges();
this.cdr.markForCheck();
}

openFiles(lock: SmbLockInfo): void {
this.files = Object.values(lock.opens);
}
}
Loading

0 comments on commit 8d5f444

Please sign in to comment.