Skip to content

Commit

Permalink
NAS-125354 / 24.04 / Added ixRequiresRoles directive (#9340)
Browse files Browse the repository at this point in the history
  • Loading branch information
RehanY147 authored Dec 19, 2023
1 parent fcf4cbc commit 5230e16
Show file tree
Hide file tree
Showing 104 changed files with 233 additions and 10 deletions.
10 changes: 10 additions & 0 deletions src/app/directives/common/common-directives.module.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { AutofocusDirective } from 'app/directives/common/autofocus/autofocus.directive';
import { HasRoleDirective } from 'app/directives/common/has-role/has-role.directive';
import { IfNightlyDirective } from 'app/directives/common/if-nightly/if-nightly.directive';
import { RequiresRolesWrapperComponent } from 'app/directives/common/requires-roles/requires-roles-wrapper.component';
import { RequiresRolesDirective } from 'app/directives/common/requires-roles/requires-roles.directive';
import { StepActivationDirective } from 'app/directives/common/step-activation.directive';
import { LetDirective } from './app-let.directive';

@NgModule({
imports: [
CommonModule,
MatTooltipModule,
TranslateModule,
],
declarations: [
LetDirective,
IfNightlyDirective,
HasRoleDirective,
RequiresRolesWrapperComponent,
RequiresRolesDirective,
AutofocusDirective,
StepActivationDirective,
],
exports: [
LetDirective,
IfNightlyDirective,
HasRoleDirective,
RequiresRolesWrapperComponent,
RequiresRolesDirective,
AutofocusDirective,
StepActivationDirective,
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

:host ::ng-deep .role-missing {
button {
cursor: none;
pointer-events: none;
}

.mat-mdc-button,
.mdc-button {
margin-right: 8px;
}

.mdc-button__label,
.mat-icon,
.mat-mdc-menu-item-text {
opacity: 0.5;
text-decoration: line-through;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component, Input, TemplateRef } from '@angular/core';

@Component({
selector: 'ix-requires-roles-wrapper',
template: `
<span [class]="['role-missing', class]" [matTooltip]="'Missing permissions for this action' | translate" matTooltipPosition="above">
<ng-container *ngTemplateOutlet="template"></ng-container>
</span>
`,
styleUrls: ['./requires-roles-wrapper.component.scss'],
})
export class RequiresRolesWrapperComponent {
@Input() template: TemplateRef<unknown>;
@Input() class: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
ComponentRef, Directive, HostBinding, Input, TemplateRef, ViewContainerRef,
} from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { RequiresRolesWrapperComponent } from 'app/directives/common/requires-roles/requires-roles-wrapper.component';
import { Role } from 'app/enums/role.enum';
import { AuthService } from 'app/services/auth/auth.service';

@UntilDestroy()
@Directive({
selector: '[ixRequiresRoles]',
})
export class RequiresRolesDirective {
private wrapperContainer: ComponentRef<RequiresRolesWrapperComponent>;

@Input()
set ixRequiresRoles(roles: Role[]) {
this.authService.hasRole(roles).pipe(untilDestroyed(this)).subscribe({
next: (hasRole) => {
if (!hasRole) {
this.wrapperContainer = this.viewContainerRef.createComponent(RequiresRolesWrapperComponent);
this.wrapperContainer.instance.template = this.templateRef;
this.wrapperContainer.instance.class = this.elementClass;
} else {
this.viewContainerRef.createEmbeddedView(this.templateRef);
}
},
});
}

protected cssClassList: string[] = [];

@Input('class')
@HostBinding('class')
get elementClass(): string {
return this.cssClassList.join(' ');
}
set(val: string): void {
this.cssClassList = val.split(' ');
}

constructor(
private templateRef: TemplateRef<unknown>,
private viewContainerRef: ViewContainerRef,
private authService: AuthService,
) { }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Observable } from 'rxjs';
import { Role } from 'app/enums/role.enum';

export interface IconActionConfig<T> {
iconName: string;
Expand All @@ -7,4 +8,5 @@ export interface IconActionConfig<T> {
dynamicTooltip?: (row: T) => Observable<string>;
hidden?: (row: T) => Observable<boolean>;
disabled?: (row: T) => Observable<boolean>;
requiresRoles?: Role[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,38 @@
<ng-container
*ngFor="let action of actions"
>
<div [matTooltip]="action.dynamicTooltip ? (action.dynamicTooltip(row) | async) : action.tooltip || ''">
<button
*ngIf="action.hidden ? !(action.hidden(row) | async) : true"
mat-icon-button
[ixTest]="[rowTestId(row), action.iconName, 'row-action']"
[disabled]="action.disabled ? (action.disabled(row) | async) : false"
(click)="$event.stopPropagation(); action.onClick(row)"
<ng-container *ngIf="action.requiresRoles?.length">
<div
*ixRequiresRoles="action.requiresRoles"
[matTooltip]="action.dynamicTooltip ? (action.dynamicTooltip(row) | async) : action.tooltip || ''"
>
<ix-icon [name]="action.iconName"></ix-icon>
</button>
</div>
<button
*ngIf="action.hidden ? !(action.hidden(row) | async) : true"
mat-icon-button
[ixTest]="[rowTestId(row), action.iconName, 'row-action']"
[disabled]="action.disabled ? (action.disabled(row) | async) : false"
(click)="$event.stopPropagation(); action.onClick(row)"
>
<ix-icon [name]="action.iconName"></ix-icon>
</button>
</div>
</ng-container>

<ng-container *ngIf="!action.requiresRoles?.length">
<div
[matTooltip]="action.dynamicTooltip ? (action.dynamicTooltip(row) | async) : action.tooltip || ''"
>
<button
*ngIf="action.hidden ? !(action.hidden(row) | async) : true"
mat-icon-button
[ixTest]="[rowTestId(row), action.iconName, 'row-action']"
[disabled]="action.disabled ? (action.disabled(row) | async) : false"
(click)="$event.stopPropagation(); action.onClick(row)"
>
<ix-icon [name]="action.iconName"></ix-icon>
</button>
</div>
</ng-container>

</ng-container>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Role } from 'app/enums/role.enum';
import { IconActionConfig } from 'app/modules/ix-table2/components/ix-table-body/cells/ix-cell-actions/icon-action-config.interface';
import { Column, ColumnComponent } from 'app/modules/ix-table2/interfaces/table-column.interface';

Expand All @@ -10,6 +11,7 @@ import { Column, ColumnComponent } from 'app/modules/ix-table2/interfaces/table-
})
export class IxCellActionsComponent<T> extends ColumnComponent<T> {
actions: IconActionConfig<T>[];
Role = Role;
}

export function actionsColumn<T>(
Expand Down
2 changes: 2 additions & 0 deletions src/app/modules/ix-table2/ix-table2.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { NgxFilesizeModule } from 'ngx-filesize';
import { CoreComponents } from 'app/core/core-components.module';
import { FormatDateTimePipe } from 'app/core/pipes/format-datetime.pipe';
import { CommonDirectivesModule } from 'app/directives/common/common-directives.module';
import { IxIconModule } from 'app/modules/ix-icon/ix-icon.module';
import { IxTable2EmptyRowComponent } from 'app/modules/ix-table2/components/ix-empty-row/ix-empty-row.component';
import { IxCellActionsComponent } from 'app/modules/ix-table2/components/ix-table-body/cells/ix-cell-actions/ix-cell-actions.component';
Expand Down Expand Up @@ -53,6 +54,7 @@ import { IxTableColumnsSelectorComponent } from './components/ix-table-columns-s
MatSlideToggleModule,
TranslateModule,
NgxFilesizeModule,
CommonDirectivesModule,
TestIdModule,
CoreComponents,
MatMenuModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
{{ image.update_available ? ('Update available' | translate) : ('Up to date' | translate) }}
</button>
<button
*ixRequiresRoles="[Role.FullAdmin]"
mat-menu-item
[ixTest]="['delete', image.repo_tags.join(', ')]"
(click)="doDelete([image])"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { of, Subject } from 'rxjs';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { mockCall, mockWebsocket } from 'app/core/testing/utils/mock-websocket.utils';
import { EntityModule } from 'app/modules/entity/entity.module';
import { IxEmptyRowHarness } from 'app/modules/ix-tables/components/ix-empty-row/ix-empty-row.component.harness';
Expand All @@ -27,6 +28,7 @@ describe('DockerImagesListComponent', () => {
IxTableModule,
],
providers: [
mockAuth(),
DockerImagesComponentStore,
mockWebsocket([
mockCall('container.image.query', fakeDockerImagesDataSource),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
delay, filter, map, switchMap,
} from 'rxjs/operators';
import { EmptyType } from 'app/enums/empty-type.enum';
import { Role } from 'app/enums/role.enum';
import { ContainerImage } from 'app/interfaces/container-image.interface';
import { IxFormatterService } from 'app/modules/ix-forms/services/ix-formatter.service';
import { IxCheckboxColumnComponent } from 'app/modules/ix-tables/components/ix-checkbox-column/ix-checkbox-column.component';
Expand All @@ -30,6 +31,7 @@ import { IxSlideInService } from 'app/services/ix-slide-in.service';
export class DockerImagesListComponent implements OnInit, AfterViewInit {
dataSource = new MatTableDataSource<ContainerImage>([]);

Role = Role;
displayedColumns = ['select', 'id', 'repo_tags', 'size', 'update', 'actions'];

@ViewChild(MatSort, { static: false }) sort: MatSort;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createComponentFactory, mockProvider } from '@ngneat/spectator/jest';
import { provideMockStore } from '@ngrx/store/testing';
import { MockComponents } from 'ng-mocks';
import { of } from 'rxjs';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { mockWebsocket, mockCall } from 'app/core/testing/utils/mock-websocket.utils';
import { ServiceName } from 'app/enums/service-name.enum';
import { ServiceStatus } from 'app/enums/service-status.enum';
Expand Down Expand Up @@ -65,6 +66,7 @@ describe('NfsCardComponent', () => {
),
],
providers: [
mockAuth(),
mockWebsocket([
mockCall('sharing.nfs.query', nfsShares),
mockCall('sharing.nfs.delete'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import {
tap, map, filter, switchMap,
} from 'rxjs';
import { Role } from 'app/enums/role.enum';
import { ServiceName } from 'app/enums/service-name.enum';
import { helptextSharingNfs } from 'app/helptext/sharing';
import { NfsShare } from 'app/interfaces/nfs-share.interface';
Expand Down Expand Up @@ -62,6 +63,7 @@ export class NfsCardComponent implements OnInit {
iconName: 'delete',
tooltip: this.translate.instant('Delete'),
onClick: (row) => this.doDelete(row),
requiresRoles: [Role.SharingNfsWrite],
},
],
}),
Expand Down
1 change: 1 addition & 0 deletions src/app/pages/sharing/nfs/nfs-form/nfs-form.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@

<ix-form-actions>
<button
*ixRequiresRoles="[Role.FullAdmin]"
mat-button
type="submit"
color="primary"
Expand Down
2 changes: 2 additions & 0 deletions src/app/pages/sharing/nfs/nfs-form/nfs-form.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Store } from '@ngrx/store';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { of } from 'rxjs';
import { MockWebsocketService } from 'app/core/testing/classes/mock-websocket.service';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { mockCall, mockWebsocket } from 'app/core/testing/utils/mock-websocket.utils';
import { NfsProtocol } from 'app/enums/nfs-protocol.enum';
import { ServiceName } from 'app/enums/service-name.enum';
Expand Down Expand Up @@ -72,6 +73,7 @@ describe('NfsFormComponent', () => {
protocols: [NfsProtocol.V3],
} as NfsConfig),
]),
mockAuth(),
mockProvider(IxSlideInService),
mockProvider(FilesystemService),
mockProvider(UserService, {
Expand Down
3 changes: 3 additions & 0 deletions src/app/pages/sharing/nfs/nfs-form/nfs-form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Observable, of } from 'rxjs';
import { DatasetPreset } from 'app/enums/dataset.enum';
import { NfsProtocol } from 'app/enums/nfs-protocol.enum';
import { NfsSecurityProvider } from 'app/enums/nfs-security-provider.enum';
import { Role } from 'app/enums/role.enum';
import { ServiceName } from 'app/enums/service-name.enum';
import { helptextSharingNfs } from 'app/helptext/sharing';
import { DatasetCreate } from 'app/interfaces/dataset.interface';
Expand Down Expand Up @@ -41,6 +42,8 @@ export class NfsFormComponent implements OnInit {
share_type: DatasetPreset.Multiprotocol,
};

Role = Role;

form = this.formBuilder.group({
path: ['', Validators.required],
comment: [''],
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/af.json
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,7 @@
"Minutes/Hours": "",
"Minutes/Hours/Days": "",
"Mirror": "",
"Missing permissions for this action": "",
"Mixed Capacity": "",
"Mixing disks of different sizes in a vdev is not recommended.": "",
"Mode": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,7 @@
"Minutes/Hours": "",
"Minutes/Hours/Days": "",
"Mirror": "",
"Missing permissions for this action": "",
"Mixed Capacity": "",
"Mixing disks of different sizes in a vdev is not recommended.": "",
"Mode": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/ast.json
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,7 @@
"Minutes/Hours": "",
"Minutes/Hours/Days": "",
"Mirror": "",
"Missing permissions for this action": "",
"Mixed Capacity": "",
"Mixing disks of different sizes in a vdev is not recommended.": "",
"Mode": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/az.json
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,7 @@
"Minutes/Hours": "",
"Minutes/Hours/Days": "",
"Mirror": "",
"Missing permissions for this action": "",
"Mixed Capacity": "",
"Mixing disks of different sizes in a vdev is not recommended.": "",
"Mode": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/be.json
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,7 @@
"Minutes/Hours": "",
"Minutes/Hours/Days": "",
"Mirror": "",
"Missing permissions for this action": "",
"Mixed Capacity": "",
"Mixing disks of different sizes in a vdev is not recommended.": "",
"Mode": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,7 @@
"Minutes/Hours": "",
"Minutes/Hours/Days": "",
"Mirror": "",
"Missing permissions for this action": "",
"Mixed Capacity": "",
"Mixing disks of different sizes in a vdev is not recommended.": "",
"Mode": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/bn.json
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,7 @@
"Minutes/Hours": "",
"Minutes/Hours/Days": "",
"Mirror": "",
"Missing permissions for this action": "",
"Mixed Capacity": "",
"Mixing disks of different sizes in a vdev is not recommended.": "",
"Mode": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/br.json
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,7 @@
"Minutes/Hours": "",
"Minutes/Hours/Days": "",
"Mirror": "",
"Missing permissions for this action": "",
"Mixed Capacity": "",
"Mixing disks of different sizes in a vdev is not recommended.": "",
"Mode": "",
Expand Down
1 change: 1 addition & 0 deletions src/assets/i18n/bs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,7 @@
"Minutes/Hours": "",
"Minutes/Hours/Days": "",
"Mirror": "",
"Missing permissions for this action": "",
"Mixed Capacity": "",
"Mixing disks of different sizes in a vdev is not recommended.": "",
"Mode": "",
Expand Down
Loading

0 comments on commit 5230e16

Please sign in to comment.