From bbc28b46599b90e9ef669bfaeeb96c44d8198042 Mon Sep 17 00:00:00 2001 From: RehanY147 Date: Mon, 6 Jan 2025 05:22:33 +0500 Subject: [PATCH 1/6] NAS-133308: Add bucket value if readonly user --- .../cloudsync-form/cloudsync-form.component.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.ts b/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.ts index 79eb1e65b4e..0174315dce6 100644 --- a/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.ts +++ b/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms'; import { MatButton } from '@angular/material/button'; import { MatCard, MatCardContent } from '@angular/material/card'; @@ -35,6 +36,7 @@ import { CloudSyncCredential } from 'app/interfaces/cloudsync-credential.interfa import { CloudSyncProvider } from 'app/interfaces/cloudsync-provider.interface'; import { newOption, SelectOption } from 'app/interfaces/option.interface'; import { ExplorerNodeData, TreeNode } from 'app/interfaces/tree-node.interface'; +import { AuthService } from 'app/modules/auth/auth.service'; import { DialogService } from 'app/modules/dialog/dialog.service'; import { CloudCredentialsSelectComponent } from 'app/modules/forms/custom-selects/cloud-credentials-select/cloud-credentials-select.component'; import { IxCheckboxComponent } from 'app/modules/forms/ix-forms/components/ix-checkbox/ix-checkbox.component'; @@ -210,6 +212,8 @@ export class CloudSyncFormComponent implements OnInit { bucketOptions$ = of([]); + private hasRequiredRoles = toSignal(this.authService.hasRole(this.requiredRoles)); + fileNodeProvider: TreeNodeProvider; bucketNodeProvider: TreeNodeProvider; @@ -229,6 +233,7 @@ export class CloudSyncFormComponent implements OnInit { private filesystemService: FilesystemService, protected cloudCredentialService: CloudCredentialService, public slideInRef: SlideInRef, + private authService: AuthService, ) { this.slideInRef.requireConfirmationWhen(() => { return of(this.form.dirty); @@ -388,6 +393,17 @@ export class CloudSyncFormComponent implements OnInit { } loadBucketOptions(): void { + if (!this.hasRequiredRoles()) { + this.isLoading = false; + const bucket = this.editingTask.attributes.bucket as string; + if (bucket) { + this.form.controls.bucket.enable(); + this.bucketOptions$ = of([{ label: bucket, value: bucket }]); + this.form.controls.bucket.setValue(bucket); + } + this.cdr.markForCheck(); + return; + } const targetCredentials = find(this.credentialsList, { id: this.form.controls.credentials.value }); this.cloudCredentialService.getBuckets(targetCredentials.id) From 1bc441e29fa6296b7c2bc6acf2131e0eee156529 Mon Sep 17 00:00:00 2001 From: RehanY147 Date: Mon, 6 Jan 2025 06:10:30 +0500 Subject: [PATCH 2/6] NAS-133308: Checkpoint --- .../ix-explorer/ix-explorer.component.html | 3 +- .../ix-explorer/ix-explorer.component.ts | 44 ++++++++++++------- .../cloudsync-form.component.html | 2 + .../cloudsync-form.component.ts | 2 +- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.html b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.html index fac156d548f..043e5dae378 100644 --- a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.html +++ b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.html @@ -42,7 +42,7 @@ @@ -79,6 +79,7 @@ + @if (loadingError) { {{ loadingError }} diff --git a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts index 5aac6ccaa45..691e523d8bb 100644 --- a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, - Component, input, + Component, computed, input, OnChanges, - OnInit, viewChild, + OnInit, Signal, viewChild, } from '@angular/core'; import { ControlValueAccessor, NgControl, ReactiveFormsModule } from '@angular/forms'; import { MatButton } from '@angular/material/button'; @@ -16,7 +16,7 @@ import { import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; import { firstValueFrom, Observable, of } from 'rxjs'; -import { catchError } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive'; import { ExplorerNodeType } from 'app/enums/explorer-type.enum'; import { mntPath } from 'app/enums/mnt-path.enum'; @@ -63,6 +63,7 @@ import { ErrorHandlerService } from 'app/services/error-handler.service'; export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAccessor { readonly label = input(); readonly hint = input(); + readonly readonly = input(false); readonly multiple = input(false); readonly tooltip = input(); readonly required = input(false); @@ -110,13 +111,18 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces }, }; - treeOptions: ITreeOptions = { - idField: 'path', - displayField: 'name', - getChildren: (node: TreeNode) => firstValueFrom(this.loadChildren(node)), - actionMapping: this.actionMapping, - useTriState: false, - }; + treeOptions: Signal = computed(() => { + const readonly = this.readonly(); + + return { + idField: 'path', + displayField: 'name', + getChildren: (node: TreeNode) => firstValueFrom(this.loadChildren(node)), + actionMapping: readonly ? {} : this.actionMapping, + useTriState: false, + useCheckbox: this.multiple(), + }; + }); constructor( public controlDirective: NgControl, @@ -129,10 +135,6 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces } ngOnChanges(changes: IxSimpleChanges): void { - if ('multiple' in changes) { - this.treeOptions.useCheckbox = this.multiple(); - } - if ('nodeProvider' in changes || 'root' in changes) { this.setInitialNode(); this.cdr.markForCheck(); @@ -159,7 +161,7 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces } setDisabledState?(isDisabled: boolean): void { - this.isDisabled = isDisabled; + this.isDisabled = isDisabled || this.readonly(); this.cdr.markForCheck(); } @@ -267,7 +269,7 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces { path: this.root(), name: this.root(), - hasChildren: true, + hasChildren: !this.readonly(), type: ExplorerNodeType.Directory, isMountpoint: true, }, @@ -279,6 +281,9 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces } private selectTreeNodes(nodeIds: string[]): void { + if (this.readonly()) { + return; + } const treeState = { ...this.tree().treeModel.getState(), selectedLeafNodeIds: nodeIds.reduce((acc, nodeId) => ({ ...acc, [nodeId]: true }), {}), @@ -288,6 +293,9 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces } private loadChildren(node: TreeNode): Observable { + if (this.readonly()) { + return of([]); + } this.loadingError = null; this.cdr.markForCheck(); @@ -296,6 +304,10 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces } return this.nodeProvider()(node).pipe( + map((childNodes) => childNodes.map((data) => { + data.hasChildren = !this.readonly() && data.hasChildren; + return data; + })), catchError((error: unknown) => { this.loadingError = this.errorHandler.getFirstErrorMessage(error); this.cdr.markForCheck(); diff --git a/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.html b/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.html index 0ac945a4e7f..a6f833eddcf 100644 --- a/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.html +++ b/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.html @@ -42,6 +42,7 @@ [label]="helptext.path_placeholder | translate" [tooltip]="helptext.path_tooltip | translate" [required]="true" + [readonly]="!hasRequiredRoles()" [nodeProvider]="fileNodeProvider" > } @@ -50,6 +51,7 @@ formControlName="path_source" [label]="helptext.path_placeholder | translate" [tooltip]="helptext.path_tooltip | translate" + [readonly]="!hasRequiredRoles()" [required]="true" [multiple]="true" [nodeProvider]="fileNodeProvider" diff --git a/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.ts b/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.ts index 0174315dce6..b4d23f422d1 100644 --- a/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.ts +++ b/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.ts @@ -212,7 +212,7 @@ export class CloudSyncFormComponent implements OnInit { bucketOptions$ = of([]); - private hasRequiredRoles = toSignal(this.authService.hasRole(this.requiredRoles)); + protected readonly hasRequiredRoles = toSignal(this.authService.hasRole(this.requiredRoles)); fileNodeProvider: TreeNodeProvider; bucketNodeProvider: TreeNodeProvider; From 7ac5de760c53cc26888ab843b6ec157740a2b8ab Mon Sep 17 00:00:00 2001 From: RehanY147 Date: Mon, 6 Jan 2025 06:22:27 +0500 Subject: [PATCH 3/6] NAS-133308: Remove some changes --- .../ix-explorer/ix-explorer.component.ts | 18 +++--------------- .../cloudsync-form.component.html | 4 ++-- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts index 691e523d8bb..f34352b599e 100644 --- a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts +++ b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.ts @@ -16,7 +16,7 @@ import { import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; import { firstValueFrom, Observable, of } from 'rxjs'; -import { catchError, map } from 'rxjs/operators'; +import { catchError } from 'rxjs/operators'; import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive'; import { ExplorerNodeType } from 'app/enums/explorer-type.enum'; import { mntPath } from 'app/enums/mnt-path.enum'; @@ -112,13 +112,11 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces }; treeOptions: Signal = computed(() => { - const readonly = this.readonly(); - return { idField: 'path', displayField: 'name', getChildren: (node: TreeNode) => firstValueFrom(this.loadChildren(node)), - actionMapping: readonly ? {} : this.actionMapping, + actionMapping: this.actionMapping, useTriState: false, useCheckbox: this.multiple(), }; @@ -269,7 +267,7 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces { path: this.root(), name: this.root(), - hasChildren: !this.readonly(), + hasChildren: true, type: ExplorerNodeType.Directory, isMountpoint: true, }, @@ -281,9 +279,6 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces } private selectTreeNodes(nodeIds: string[]): void { - if (this.readonly()) { - return; - } const treeState = { ...this.tree().treeModel.getState(), selectedLeafNodeIds: nodeIds.reduce((acc, nodeId) => ({ ...acc, [nodeId]: true }), {}), @@ -293,9 +288,6 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces } private loadChildren(node: TreeNode): Observable { - if (this.readonly()) { - return of([]); - } this.loadingError = null; this.cdr.markForCheck(); @@ -304,10 +296,6 @@ export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAcces } return this.nodeProvider()(node).pipe( - map((childNodes) => childNodes.map((data) => { - data.hasChildren = !this.readonly() && data.hasChildren; - return data; - })), catchError((error: unknown) => { this.loadingError = this.errorHandler.getFirstErrorMessage(error); this.cdr.markForCheck(); diff --git a/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.html b/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.html index a6f833eddcf..034338adb4e 100644 --- a/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.html +++ b/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.html @@ -42,7 +42,6 @@ [label]="helptext.path_placeholder | translate" [tooltip]="helptext.path_tooltip | translate" [required]="true" - [readonly]="!hasRequiredRoles()" [nodeProvider]="fileNodeProvider" > } @@ -51,7 +50,6 @@ formControlName="path_source" [label]="helptext.path_placeholder | translate" [tooltip]="helptext.path_tooltip | translate" - [readonly]="!hasRequiredRoles()" [required]="true" [multiple]="true" [nodeProvider]="fileNodeProvider" @@ -99,6 +97,7 @@ Date: Mon, 6 Jan 2025 06:41:46 +0500 Subject: [PATCH 4/6] NAS-133308: Update ix-explorer.component.spec.ts --- .../components/ix-explorer/ix-explorer.component.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.spec.ts b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.spec.ts index 4289c50f294..6e09869ed01 100644 --- a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.spec.ts +++ b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.spec.ts @@ -358,5 +358,8 @@ describe.skip('IxExplorerComponent', () => { expect(spectator.query('input')).toBeDisabled(); expect(spectator.query('.tree-container')).toHaveClass('disabled'); }); + + // TODO: Add test when overall tests for the component are working, after the following issue is solved + // https://github.com/help-me-mom/ng-mocks/issues/10503 }); }); From 7cc9dafb25f659a9c938b22a5012efca90515e3b Mon Sep 17 00:00:00 2001 From: RehanY147 Date: Mon, 6 Jan 2025 06:42:42 +0500 Subject: [PATCH 5/6] NAS-133308: Update ix-explorer.component.spec.ts --- .../components/ix-explorer/ix-explorer.component.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.spec.ts b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.spec.ts index 6e09869ed01..5777b869d23 100644 --- a/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.spec.ts +++ b/src/app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component.spec.ts @@ -359,7 +359,8 @@ describe.skip('IxExplorerComponent', () => { expect(spectator.query('.tree-container')).toHaveClass('disabled'); }); - // TODO: Add test when overall tests for the component are working, after the following issue is solved + // TODO: Add test 'disables input when readonly is set to true on ix-explorer' + // when overall tests for the component are working, after the following issue is solved // https://github.com/help-me-mom/ng-mocks/issues/10503 }); }); From 2bd9350334e39c7dc067140c65b03a00fef607e1 Mon Sep 17 00:00:00 2001 From: RehanY147 Date: Wed, 8 Jan 2025 04:47:17 +0500 Subject: [PATCH 6/6] NAS-133308: Added tests --- .../cloudsync-form.component.spec.ts | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.spec.ts b/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.spec.ts index 9bf3201ee33..9f14dd72c25 100644 --- a/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.spec.ts +++ b/src/app/pages/data-protection/cloudsync/cloudsync-form/cloudsync-form.component.spec.ts @@ -13,10 +13,12 @@ import { mntPath } from 'app/enums/mnt-path.enum'; import { TransferMode } from 'app/enums/transfer-mode.enum'; import { CloudSyncTaskUi } from 'app/interfaces/cloud-sync-task.interface'; import { CloudSyncCredential } from 'app/interfaces/cloudsync-credential.interface'; +import { AuthService } from 'app/modules/auth/auth.service'; import { DialogService } from 'app/modules/dialog/dialog.service'; import { CloudCredentialsSelectComponent, } from 'app/modules/forms/custom-selects/cloud-credentials-select/cloud-credentials-select.component'; +import { IxSelectHarness } from 'app/modules/forms/ix-forms/components/ix-select/ix-select.harness'; import { SlideIn } from 'app/modules/slide-ins/slide-in'; import { SlideInRef } from 'app/modules/slide-ins/slide-in-ref'; import { ApiService } from 'app/modules/websocket/api.service'; @@ -24,6 +26,7 @@ import { CloudSyncFormComponent } from 'app/pages/data-protection/cloudsync/clou import { TransferModeExplanationComponent, } from 'app/pages/data-protection/cloudsync/transfer-mode-explanation/transfer-mode-explanation.component'; +import { CloudCredentialService } from 'app/services/cloud-credential.service'; import { FilesystemService } from 'app/services/filesystem.service'; describe('CloudSyncFormComponent', () => { @@ -76,6 +79,59 @@ describe('CloudSyncFormComponent', () => { state: { state: JobState.Pending }, } as CloudSyncTaskUi; + const existingTask2 = { + id: 1, + description: 'test3', + path: '/mnt/dozer', + attributes: { + folder: '/', + bucket: 'test3', + fast_list: false, + }, + next_run: 'Disabled', + pre_script: '', + post_script: '', + snapshot: false, + include: [], + exclude: [], + args: '', + enabled: true, + job: null, + direction: 'PULL', + transfer_mode: 'COPY', + bwlimit: [], + transfers: 4, + encryption: false, + filename_encryption: false, + encryption_password: '', + encryption_salt: '', + create_empty_src_dirs: false, + follow_symlinks: false, + credentials: { + id: 1, + name: 'Storj', + provider: { + type: 'STORJ_IX', + access_key_id: 'julzdrlwyv37oixflnbyysbumg3q', + secret_access_key: 'jyncyw7oup4ad2fv3tectsaksdag73oi7633arrzdlj77gmmywmvo', + }, + }, + schedule: { + minute: '0', + hour: '0', + dom: '*', + month: '*', + dow: '*', + }, + locked: false, + credential: 'Storj', + next_run_time: '2025-01-08T08:00:00.000Z', + state: { + state: 'PENDING', + }, + last_run: 'Disabled', + } as CloudSyncTaskUi; + let loader: HarnessLoader; let spectator: Spectator; const getData = jest.fn(() => existingTask); @@ -280,4 +336,84 @@ describe('CloudSyncFormComponent', () => { expect(slideInRef.close).toHaveBeenCalledWith({ response: existingTask, error: null }); }); }); + describe('doesnt load buckets when user doesnt has roles', () => { + beforeEach(() => { + spectator = createComponent({ + providers: [ + mockProvider(SlideInRef, { + ...slideInRef, + getData: jest.fn(() => existingTask2), + }), + mockProvider(CloudCredentialService, { + getProviders: jest.fn(() => { + return of([{ + name: CloudSyncProviderName.Http, + title: 'Http', + buckets: false, + bucket_title: 'Bucket', + task_schema: [], + credentials_schema: [], + credentials_oauth: null, + }, + { + name: CloudSyncProviderName.Mega, + title: 'Mega', + buckets: false, + bucket_title: 'Bucket', + task_schema: [], + credentials_schema: [], + credentials_oauth: null, + }, + { + name: CloudSyncProviderName.Storj, + title: 'Storj iX', + credentials_oauth: null, + credentials_schema: [], + buckets: true, + bucket_title: 'Bucket', + task_schema: [ + { + property: 'fast_list', + schema: { + type: 'boolean', + _name_: 'fast_list', + title: 'Use --fast-list', + description: 'Use fewer transactions in exchange for more RAM. This may also speed up or slow down your\ntransfer. See [rclone documentation](https://rclone.org/docs/#fast-list) for more details.', + default: false, + _required_: false, + }, + }, + ], + }]); + }), + getCloudSyncCredentials: jest.fn(() => { + return of([ + { + id: 1, + name: 'Storj', + provider: { + type: CloudSyncProviderName.Storj, + url: '', + access_key_id: 'julzdrlwyv37oixflnbyysbumg3q', + secret_access_key: 'jyncyw7oup4ad2fv3tectsaksdag73oi7633arrzdlj77gmmywmvo', + }, + }, + ]); + }), + }), + mockProvider(AuthService, { + hasRole: jest.fn(() => of(false)), + }), + ], + }); + loader = TestbedHarnessEnvironment.loader(spectator.fixture); + spectator.detectChanges(); + }); + + it('doesnt load buckets', async () => { + const buckets = await loader.getHarness(IxSelectHarness.with({ label: 'Bucket' })); + const options = await buckets.getOptionLabels(); + expect(options).toEqual(['--', 'test3']); + }); + }); });