-
Notifications
You must be signed in to change notification settings - Fork 321
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NAS-133675: Refactor CloudBackupListComponent to master-detail-view (#…
…11429) * NAS-133675: Refactor CloudBackupListComponent to master-detail-view * NAS-133675: Refactor CloudBackupListComponent to master-detail-view * NAS-133675: Refactor CloudBackupListComponent to master-detail-view * NAS-133675: Refactor CloudBackupListComponent to master-detail-view * NAS-133675: Skip tests * NAS-133675: Update * NAS-133675: Listen for cloudbackup changes * NAS-133675: Listen for cloudbackup changes * NAS-133675: Update tests * NAS-133675: Update tests (cherry picked from commit fd36ee9)
- Loading branch information
1 parent
7962fcf
commit 8d5d426
Showing
13 changed files
with
560 additions
and
448 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
...app/pages/data-protection/cloud-backup/all-cloud-backups/all-cloud-backups.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<ix-page-header> | ||
<button | ||
*ixRequiresRoles="requiredRoles" | ||
mat-button | ||
color="primary" | ||
ixTest="add-task" | ||
[ixUiSearch]="searchableElements.elements.add" | ||
(click)="openForm()" | ||
> | ||
{{ 'Add' | translate }} | ||
</button> | ||
</ix-page-header> | ||
|
||
<ix-master-detail-view | ||
#masterDetailView="masterDetailViewContext" | ||
[selectedItem]="dataProvider?.expandedRow" | ||
> | ||
<ix-cloud-backup-list | ||
master | ||
[dataProvider]="dataProvider" | ||
[cloudBackups]="cloudBackups()" | ||
[isMobileView]="masterDetailView.isMobileView()" | ||
(toggleShowMobileDetails)="masterDetailView.toggleShowMobileDetails($event)" | ||
></ix-cloud-backup-list> | ||
|
||
<ng-container detail-header> | ||
{{ 'Task Details for {task}' | translate: { task: dataProvider?.expandedRow?.description} }} | ||
</ng-container> | ||
|
||
<ng-container detail> | ||
@if (dataProvider.expandedRow) { | ||
<ix-cloud-backup-details [backup]="dataProvider.expandedRow"></ix-cloud-backup-details> | ||
} | ||
</ng-container> | ||
</ix-master-detail-view> |
Empty file.
221 changes: 221 additions & 0 deletions
221
.../pages/data-protection/cloud-backup/all-cloud-backups/all-cloud-backups.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
import { HarnessLoader } from '@angular/cdk/testing'; | ||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; | ||
import { MatButtonHarness } from '@angular/material/button/testing'; | ||
import { MatSlideToggleHarness } from '@angular/material/slide-toggle/testing'; | ||
import { Spectator, createComponentFactory, mockProvider } from '@ngneat/spectator/jest'; | ||
import { provideMockStore } from '@ngrx/store/testing'; | ||
import { MockComponents, MockDirective } from 'ng-mocks'; | ||
import { of } from 'rxjs'; | ||
import { mockApi, mockCall, mockJob } from 'app/core/testing/utils/mock-api.utils'; | ||
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils'; | ||
import { DetailsHeightDirective } from 'app/directives/details-height/details-height.directive'; | ||
import { JobState } from 'app/enums/job-state.enum'; | ||
import { AdvancedConfig } from 'app/interfaces/advanced-config.interface'; | ||
import { CloudBackup } from 'app/interfaces/cloud-backup.interface'; | ||
import { DialogService } from 'app/modules/dialog/dialog.service'; | ||
import { IxIconHarness } from 'app/modules/ix-icon/ix-icon.harness'; | ||
import { IxTableHarness } from 'app/modules/ix-table/components/ix-table/ix-table.harness'; | ||
import { SortDirection } from 'app/modules/ix-table/enums/sort-direction.enum'; | ||
import { selectJobs } from 'app/modules/jobs/store/job.selectors'; | ||
import { MasterDetailViewComponent } from 'app/modules/master-detail-view/master-detail-view.component'; | ||
import { PageHeaderComponent } from 'app/modules/page-header/page-title-header/page-header.component'; | ||
import { SlideIn } from 'app/modules/slide-ins/slide-in'; | ||
import { ApiService } from 'app/modules/websocket/api.service'; | ||
import { AllCloudBackupsComponent } from 'app/pages/data-protection/cloud-backup/all-cloud-backups/all-cloud-backups.component'; | ||
import { CloudBackupDetailsComponent } from 'app/pages/data-protection/cloud-backup/cloud-backup-details/cloud-backup-details.component'; | ||
import { CloudBackupFormComponent } from 'app/pages/data-protection/cloud-backup/cloud-backup-form/cloud-backup-form.component'; | ||
import { selectPreferences } from 'app/store/preferences/preferences.selectors'; | ||
import { selectAdvancedConfig, selectSystemConfigState } from 'app/store/system-config/system-config.selectors'; | ||
|
||
describe('AllCloudBackupsComponent', () => { | ||
let spectator: Spectator<AllCloudBackupsComponent>; | ||
let loader: HarnessLoader; | ||
let table: IxTableHarness; | ||
|
||
const cloudBackups = [ | ||
{ | ||
id: 1, | ||
description: 'UA', | ||
path: '/mnt/nmnmn', | ||
pre_script: 'your_pre_script', | ||
snapshot: false, | ||
enabled: false, | ||
job: { | ||
state: JobState.Finished, | ||
time_finished: { | ||
$date: new Date().getTime() - 50000, | ||
}, | ||
}, | ||
}, | ||
{ | ||
id: 2, | ||
description: 'UAH', | ||
path: '/mnt/hahah', | ||
pre_script: 'your_pre_script', | ||
snapshot: false, | ||
enabled: true, | ||
job: { | ||
state: JobState.Finished, | ||
time_finished: { | ||
$date: new Date().getTime() - 50000, | ||
}, | ||
}, | ||
}, | ||
] as CloudBackup[]; | ||
|
||
const createComponent = createComponentFactory({ | ||
component: AllCloudBackupsComponent, | ||
imports: [ | ||
MockComponents( | ||
PageHeaderComponent, | ||
CloudBackupDetailsComponent, | ||
), | ||
MockDirective(DetailsHeightDirective), | ||
], | ||
providers: [ | ||
mockAuth(), | ||
mockApi([ | ||
mockCall('cloud_backup.query', cloudBackups), | ||
mockCall('cloud_backup.delete'), | ||
mockCall('cloud_backup.update'), | ||
mockJob('cloud_backup.sync'), | ||
]), | ||
mockProvider(DialogService, { | ||
confirm: jest.fn(() => of(true)), | ||
}), | ||
mockProvider(SlideIn, { | ||
open: jest.fn(() => of({ | ||
response: true, | ||
})), | ||
}), | ||
provideMockStore({ | ||
selectors: [ | ||
{ | ||
selector: selectSystemConfigState, | ||
value: {}, | ||
}, | ||
{ | ||
selector: selectPreferences, | ||
value: {}, | ||
}, | ||
{ | ||
selector: selectJobs, | ||
value: [{ | ||
state: JobState.Finished, | ||
time_finished: { | ||
$date: new Date().getTime() - 50000, | ||
}, | ||
}], | ||
}, | ||
{ | ||
selector: selectAdvancedConfig, | ||
value: { | ||
consolemenu: true, | ||
serialconsole: true, | ||
serialport: 'ttyS0', | ||
serialspeed: '9600', | ||
motd: 'Welcome back, commander', | ||
} as AdvancedConfig, | ||
}, | ||
], | ||
}), | ||
], | ||
}); | ||
|
||
beforeEach(async () => { | ||
spectator = createComponent(); | ||
loader = TestbedHarnessEnvironment.loader(spectator.fixture); | ||
table = await loader.getHarness(IxTableHarness); | ||
}); | ||
|
||
it('checks used components on page', () => { | ||
expect(spectator.query(PageHeaderComponent)).toExist(); | ||
expect(spectator.query(MasterDetailViewComponent)).toExist(); | ||
}); | ||
|
||
it('shows form to create new Cloud Backup when Add button is pressed', async () => { | ||
const addButton = await loader.getHarness(MatButtonHarness.with({ text: 'Add' })); | ||
await addButton.click(); | ||
|
||
expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith( | ||
CloudBackupFormComponent, | ||
{ wide: true }, | ||
); | ||
}); | ||
|
||
describe('cloud backup list', () => { | ||
it('should show table rows', async () => { | ||
const expectedRows = [ | ||
['Name', 'Enabled', 'Snapshot', 'State', 'Last Run', ''], | ||
['UA', '', 'No', 'FINISHED', '1 min. ago', ''], | ||
['UAH', '', 'No', 'FINISHED', '1 min. ago', ''], | ||
]; | ||
const cells = await table.getCellTexts(); | ||
expect(cells).toEqual(expectedRows); | ||
}); | ||
|
||
it('sets the default sort for dataProvider', () => { | ||
spectator.component.dataProvider.load(); | ||
|
||
expect(spectator.component.dataProvider.sorting).toEqual({ | ||
active: 1, | ||
direction: SortDirection.Asc, | ||
propertyName: 'description', | ||
}); | ||
}); | ||
|
||
it('shows form to edit an existing Cloud Backup when Edit button is pressed', async () => { | ||
const editButton = await table.getHarnessInCell(IxIconHarness.with({ name: 'edit' }), 1, 5); | ||
await editButton.click(); | ||
|
||
expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith( | ||
CloudBackupFormComponent, | ||
{ | ||
wide: true, | ||
data: cloudBackups[0], | ||
}, | ||
); | ||
}); | ||
|
||
it('shows confirmation dialog when Run Now button is pressed', async () => { | ||
const runNowButton = await table.getHarnessInCell(IxIconHarness.with({ name: 'mdi-play-circle' }), 1, 5); | ||
await runNowButton.click(); | ||
|
||
expect(spectator.inject(DialogService).confirm).toHaveBeenCalledWith({ | ||
title: 'Run Now', | ||
message: 'Run «UA» Cloud Backup now?', | ||
hideCheckbox: true, | ||
}); | ||
|
||
expect(spectator.inject(ApiService).job).toHaveBeenCalledWith('cloud_backup.sync', [1]); | ||
expect(spectator.component.dataProvider.expandedRow).toEqual({ ...cloudBackups[0] }); | ||
}); | ||
|
||
it('deletes a Cloud Backup with confirmation when Delete button is pressed', async () => { | ||
const deleteIcon = await table.getHarnessInCell(IxIconHarness.with({ name: 'mdi-delete' }), 1, 5); | ||
await deleteIcon.click(); | ||
|
||
expect(spectator.inject(DialogService).confirm).toHaveBeenCalledWith({ | ||
title: 'Confirmation', | ||
message: 'Delete Cloud Backup <b>"UA"</b>?', | ||
buttonColor: 'warn', | ||
buttonText: 'Delete', | ||
}); | ||
|
||
expect(spectator.inject(ApiService).call).toHaveBeenCalledWith('cloud_backup.delete', [1]); | ||
}); | ||
|
||
it('updates Cloud Backup Enabled status once mat-toggle is updated', async () => { | ||
const toggle = await table.getHarnessInCell(MatSlideToggleHarness, 1, 1); | ||
|
||
expect(await toggle.isChecked()).toBe(false); | ||
|
||
await toggle.check(); | ||
|
||
expect(spectator.inject(ApiService).call).toHaveBeenCalledWith( | ||
'cloud_backup.update', | ||
[1, { enabled: true }], | ||
); | ||
}); | ||
}); | ||
}); |
108 changes: 108 additions & 0 deletions
108
src/app/pages/data-protection/cloud-backup/all-cloud-backups/all-cloud-backups.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { | ||
Component, ChangeDetectionStrategy, signal, OnInit, | ||
ChangeDetectorRef, | ||
} from '@angular/core'; | ||
import { MatButton } from '@angular/material/button'; | ||
import { ActivatedRoute, NavigationStart, Router } from '@angular/router'; | ||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; | ||
import { TranslateModule } from '@ngx-translate/core'; | ||
import { filter, tap } from 'rxjs'; | ||
import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive'; | ||
import { UiSearchDirective } from 'app/directives/ui-search.directive'; | ||
import { Role } from 'app/enums/role.enum'; | ||
import { CloudBackup } from 'app/interfaces/cloud-backup.interface'; | ||
import { AsyncDataProvider } from 'app/modules/ix-table/classes/async-data-provider/async-data-provider'; | ||
import { SortDirection } from 'app/modules/ix-table/enums/sort-direction.enum'; | ||
import { MasterDetailViewComponent } from 'app/modules/master-detail-view/master-detail-view.component'; | ||
import { PageHeaderComponent } from 'app/modules/page-header/page-title-header/page-header.component'; | ||
import { SlideIn } from 'app/modules/slide-ins/slide-in'; | ||
import { ApiService } from 'app/modules/websocket/api.service'; | ||
import { CloudBackupDetailsComponent } from 'app/pages/data-protection/cloud-backup/cloud-backup-details/cloud-backup-details.component'; | ||
import { CloudBackupFormComponent } from 'app/pages/data-protection/cloud-backup/cloud-backup-form/cloud-backup-form.component'; | ||
import { CloudBackupListComponent } from 'app/pages/data-protection/cloud-backup/cloud-backup-list/cloud-backup-list.component'; | ||
import { cloudBackupListElements } from 'app/pages/data-protection/cloud-backup/cloud-backup-list/cloud-backup-list.elements'; | ||
|
||
@UntilDestroy() | ||
@Component({ | ||
selector: 'ix-all-cloud-backups', | ||
templateUrl: './all-cloud-backups.component.html', | ||
styleUrls: ['./all-cloud-backups.component.scss'], | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
standalone: true, | ||
imports: [ | ||
MasterDetailViewComponent, | ||
CloudBackupListComponent, | ||
CloudBackupDetailsComponent, | ||
PageHeaderComponent, | ||
TranslateModule, | ||
UiSearchDirective, | ||
RequiresRolesDirective, | ||
MatButton, | ||
], | ||
}) | ||
export class AllCloudBackupsComponent implements OnInit { | ||
dataProvider: AsyncDataProvider<CloudBackup>; | ||
protected readonly cloudBackups = signal<CloudBackup[]>([]); | ||
protected readonly searchableElements = cloudBackupListElements; | ||
readonly requiredRoles = [Role.CloudBackupWrite]; | ||
|
||
constructor( | ||
private api: ApiService, | ||
private slideIn: SlideIn, | ||
private route: ActivatedRoute, | ||
private cdr: ChangeDetectorRef, | ||
private router: Router, | ||
) { | ||
this.router.events | ||
.pipe(filter((event) => event instanceof NavigationStart), untilDestroyed(this)) | ||
.subscribe(() => { | ||
if (this.router.getCurrentNavigation()?.extras?.state?.hideMobileDetails) { | ||
this.dataProvider.expandedRow = null; | ||
this.cdr.markForCheck(); | ||
} | ||
}); | ||
} | ||
|
||
ngOnInit(): void { | ||
this.route.fragment.pipe( | ||
tap((id) => this.loadCloudBackups(id || undefined)), | ||
untilDestroyed(this), | ||
).subscribe(); | ||
} | ||
|
||
openForm(row?: CloudBackup): void { | ||
this.slideIn.open(CloudBackupFormComponent, { data: row, wide: true }) | ||
.pipe( | ||
filter((response) => !!response.response), | ||
untilDestroyed(this), | ||
).subscribe(() => this.dataProvider.load()); | ||
} | ||
|
||
private loadCloudBackups(id?: string): void { | ||
const cloudBackups$ = this.api.call('cloud_backup.query').pipe( | ||
tap((cloudBackups) => { | ||
this.cloudBackups.set(cloudBackups); | ||
|
||
const selectedBackup = id | ||
? cloudBackups.find((cloudBackup) => cloudBackup.id.toString() === id) | ||
: cloudBackups.find((cloudBackup) => cloudBackup.id === this.dataProvider?.expandedRow?.id); | ||
|
||
if (selectedBackup) { | ||
this.dataProvider.expandedRow = selectedBackup; | ||
} else if (cloudBackups.length) { | ||
const [firstBackup] = cloudBackups; | ||
this.dataProvider.expandedRow = firstBackup; | ||
} | ||
this.cdr.markForCheck(); | ||
}), | ||
); | ||
|
||
this.dataProvider = new AsyncDataProvider<CloudBackup>(cloudBackups$); | ||
this.dataProvider.setSorting({ | ||
active: 1, | ||
direction: SortDirection.Asc, | ||
propertyName: 'description', | ||
}); | ||
this.dataProvider.load(); | ||
} | ||
} |
15 changes: 0 additions & 15 deletions
15
...ges/data-protection/cloud-backup/cloud-backup-details/cloud-backup-details.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.