-
Notifications
You must be signed in to change notification settings - Fork 309
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NAS-132583 / 25.04 / Add mass actions to containers (#11072)
* NAS-132583: Add mass actions to containers * NAS-132583: PR Update * NAS-132583: PR Update
- Loading branch information
1 parent
f448132
commit d4af2de
Showing
96 changed files
with
605 additions
and
15 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
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 |
---|---|---|
|
@@ -87,7 +87,6 @@ | |
} | ||
|
||
.table-container { | ||
background: var(--bg2); | ||
box-sizing: border-box; | ||
display: flex; | ||
flex: 1; | ||
|
49 changes: 49 additions & 0 deletions
49
...tances/instance-list/instance-list-bulk-actions/instance-list-bulk-actions.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,49 @@ | ||
<div class="bulk-actions-container"> | ||
<div class="bulk-selected"> | ||
<span>{{ checkedInstances().length }}</span> | ||
<span>{{ 'Selected' | translate }}</span> | ||
</div> | ||
|
||
<div class="bulk-button-wrapper"> | ||
<label>{{ 'Bulk Actions' | translate }}</label> | ||
<button | ||
*ixRequiresRoles="requiredRoles" | ||
mat-button | ||
ixTest="bulk-actions-menu" | ||
[matMenuTriggerFor]="menu" | ||
> | ||
{{ 'Select action' | translate }} | ||
<ix-icon name="mdi-menu-down" class="menu-caret"></ix-icon> | ||
</button> | ||
</div> | ||
|
||
<mat-menu #menu="matMenu"> | ||
<button | ||
*ixRequiresRoles="requiredRoles" | ||
mat-menu-item | ||
ixTest="start-selected" | ||
[disabled]="isBulkStartDisabled()" | ||
(click)="onBulkStart()" | ||
> | ||
<span>{{ 'Start All Selected' | translate }}</span> | ||
</button> | ||
<button | ||
*ixRequiresRoles="requiredRoles" | ||
mat-menu-item | ||
ixTest="stop-selected" | ||
[disabled]="isBulkStopDisabled()" | ||
(click)="onBulkStop()" | ||
> | ||
<span>{{ 'Stop All Selected' | translate }}</span> | ||
</button> | ||
<button | ||
*ixRequiresRoles="requiredRoles" | ||
mat-menu-item | ||
ixTest="restart-selected" | ||
[disabled]="isBulkStopDisabled()" | ||
(click)="onBulkRestart()" | ||
> | ||
<span>{{ 'Restart All Selected' | translate }}</span> | ||
</button> | ||
</mat-menu> | ||
</div> |
31 changes: 31 additions & 0 deletions
31
...tances/instance-list/instance-list-bulk-actions/instance-list-bulk-actions.component.scss
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,31 @@ | ||
.bulk-selected { | ||
align-items: center; | ||
align-self: flex-end; | ||
display: inline-flex; | ||
font-size: 16px; | ||
gap: 4px; | ||
height: 36px; | ||
} | ||
|
||
.bulk-actions-container { | ||
align-items: flex-end; | ||
display: flex; | ||
gap: 12px; | ||
} | ||
|
||
.bulk-button-wrapper { | ||
display: flex; | ||
flex-direction: column; | ||
|
||
label { | ||
color: var(--fg2); | ||
font-size: 10px; | ||
margin-bottom: 2px; | ||
} | ||
|
||
button { | ||
background-color: var(--bg1); | ||
border: 1px solid var(--lines); | ||
font-size: 12px; | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
...ces/instance-list/instance-list-bulk-actions/instance-list-bulk-actions.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,90 @@ | ||
import { HarnessLoader } from '@angular/cdk/testing'; | ||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; | ||
import { MatDialog } from '@angular/material/dialog'; | ||
import { MatMenuModule } from '@angular/material/menu'; | ||
import { MatMenuHarness } from '@angular/material/menu/testing'; | ||
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; | ||
import { of } from 'rxjs'; | ||
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils'; | ||
import { VirtualizationInstance } from 'app/interfaces/virtualization.interface'; | ||
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service'; | ||
import { StopOptionsDialogComponent, StopOptionsOperation } from 'app/pages/virtualization/components/all-instances/instance-list/stop-options-dialog/stop-options-dialog.component'; | ||
import { ErrorHandlerService } from 'app/services/error-handler.service'; | ||
import { InstanceListBulkActionsComponent } from './instance-list-bulk-actions.component'; | ||
|
||
describe('InstanceListBulkActionsComponent', () => { | ||
let spectator: Spectator<InstanceListBulkActionsComponent>; | ||
let loader: HarnessLoader; | ||
let menu: MatMenuHarness; | ||
|
||
const checkedInstancesMock = [ | ||
{ id: '1', status: 'Running' }, | ||
{ id: '2', status: 'Stopped' }, | ||
] as unknown as VirtualizationInstance[]; | ||
|
||
const createComponent = createComponentFactory({ | ||
component: InstanceListBulkActionsComponent, | ||
imports: [MatMenuModule], | ||
providers: [ | ||
mockProvider(SnackbarService), | ||
mockAuth(), | ||
mockProvider(MatDialog, { | ||
open: jest.fn(() => ({ | ||
afterClosed: jest.fn(() => of(true)), | ||
})), | ||
}), | ||
mockProvider(ErrorHandlerService), | ||
], | ||
}); | ||
|
||
beforeEach(async () => { | ||
spectator = createComponent({ | ||
props: { | ||
checkedInstances: checkedInstancesMock, | ||
}, | ||
}); | ||
loader = TestbedHarnessEnvironment.loader(spectator.fixture); | ||
menu = await loader.getHarness(MatMenuHarness); | ||
await menu.open(); | ||
}); | ||
|
||
it('displays the correct count of selected instances', () => { | ||
const selectedCount = spectator.query('.bulk-selected span:first-child'); | ||
expect(selectedCount).toHaveText(String(checkedInstancesMock.length)); | ||
}); | ||
|
||
it('calls onBulkStart when Start All Selected is clicked', async () => { | ||
const startSpy = jest.spyOn(spectator.component, 'onBulkStart'); | ||
|
||
await menu.open(); | ||
await menu.clickItem({ text: 'Start All Selected' }); | ||
|
||
expect(startSpy).toHaveBeenCalled(); | ||
expect(spectator.inject(SnackbarService).success).toHaveBeenCalledWith('Requested action performed for selected Instances'); | ||
}); | ||
|
||
it('opens the Stop Options dialog when Stop All Selected is clicked', async () => { | ||
const matDialog = spectator.inject(MatDialog); | ||
|
||
await menu.open(); | ||
await menu.clickItem({ text: 'Stop All Selected' }); | ||
|
||
expect(matDialog.open).toHaveBeenCalledWith(StopOptionsDialogComponent, { data: StopOptionsOperation.Stop }); | ||
}); | ||
|
||
it('opens the Restart Options dialog when Restart All Selected is clicked', async () => { | ||
const matDialog = spectator.inject(MatDialog); | ||
|
||
await menu.open(); | ||
await menu.clickItem({ text: 'Restart All Selected' }); | ||
|
||
expect(matDialog.open).toHaveBeenCalledWith(StopOptionsDialogComponent, { data: StopOptionsOperation.Restart }); | ||
}); | ||
|
||
it('emits resetBulkSelection after actions', () => { | ||
const resetSpy = jest.spyOn(spectator.component.resetBulkSelection, 'emit'); | ||
|
||
spectator.component.onBulkStart(); | ||
expect(resetSpy).toHaveBeenCalled(); | ||
}); | ||
}); |
140 changes: 140 additions & 0 deletions
140
...nstances/instance-list/instance-list-bulk-actions/instance-list-bulk-actions.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,140 @@ | ||
import { | ||
Component, | ||
ChangeDetectionStrategy, | ||
computed, | ||
input, | ||
output, | ||
} from '@angular/core'; | ||
import { MatButton } from '@angular/material/button'; | ||
import { MatDialog } from '@angular/material/dialog'; | ||
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; | ||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; | ||
import { TranslateModule, TranslateService } from '@ngx-translate/core'; | ||
import { | ||
filter, | ||
tap, | ||
} from 'rxjs'; | ||
import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive'; | ||
import { Role } from 'app/enums/role.enum'; | ||
import { VirtualizationStatus } from 'app/enums/virtualization.enum'; | ||
import { VirtualizationInstance, VirtualizationStopParams } from 'app/interfaces/virtualization.interface'; | ||
import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component'; | ||
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service'; | ||
import { TestDirective } from 'app/modules/test-id/test.directive'; | ||
import { StopOptionsDialogComponent, StopOptionsOperation } from 'app/pages/virtualization/components/all-instances/instance-list/stop-options-dialog/stop-options-dialog.component'; | ||
import { ErrorHandlerService } from 'app/services/error-handler.service'; | ||
import { ApiService } from 'app/services/websocket/api.service'; | ||
|
||
@UntilDestroy() | ||
@Component({ | ||
selector: 'ix-instance-list-bulk-actions', | ||
templateUrl: './instance-list-bulk-actions.component.html', | ||
styleUrls: ['./instance-list-bulk-actions.component.scss'], | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
standalone: true, | ||
imports: [ | ||
MatButton, | ||
IxIconComponent, | ||
MatMenu, | ||
MatMenuItem, | ||
MatMenuTrigger, | ||
TranslateModule, | ||
RequiresRolesDirective, | ||
TestDirective, | ||
], | ||
}) | ||
|
||
export class InstanceListBulkActionsComponent { | ||
readonly checkedInstances = input<VirtualizationInstance[]>(); | ||
readonly resetBulkSelection = output(); | ||
|
||
protected readonly requiredRoles = [Role.VirtInstanceWrite]; | ||
|
||
readonly bulkActionStartedMessage = this.translate.instant('Requested action performed for selected Instances'); | ||
|
||
protected readonly isBulkStartDisabled = computed(() => { | ||
return this.checkedInstances().every( | ||
(instance) => [VirtualizationStatus.Running].includes(instance.status), | ||
); | ||
}); | ||
|
||
protected readonly isBulkStopDisabled = computed(() => { | ||
return this.checkedInstances().every( | ||
(instance) => [VirtualizationStatus.Stopped].includes(instance.status), | ||
); | ||
}); | ||
|
||
protected readonly activeCheckedInstances = computed(() => { | ||
return this.checkedInstances().filter( | ||
(instance) => [VirtualizationStatus.Running].includes(instance.status), | ||
); | ||
}); | ||
|
||
protected readonly stoppedCheckedInstances = computed(() => { | ||
return this.checkedInstances().filter( | ||
(instance) => [VirtualizationStatus.Stopped].includes(instance.status), | ||
); | ||
}); | ||
|
||
constructor( | ||
private translate: TranslateService, | ||
private snackbar: SnackbarService, | ||
private api: ApiService, | ||
private errorHandler: ErrorHandlerService, | ||
private matDialog: MatDialog, | ||
) {} | ||
|
||
onBulkStart(): void { | ||
this.stoppedCheckedInstances().forEach((instance) => this.start(instance.id)); | ||
this.resetBulkSelection.emit(); | ||
this.snackbar.success(this.translate.instant(this.bulkActionStartedMessage)); | ||
} | ||
|
||
onBulkStop(): void { | ||
this.matDialog | ||
.open(StopOptionsDialogComponent, { data: StopOptionsOperation.Stop }) | ||
.afterClosed() | ||
.pipe( | ||
filter(Boolean), | ||
tap((options: VirtualizationStopParams) => { | ||
this.activeCheckedInstances().forEach((instance) => this.stop(instance.id, options)); | ||
this.snackbar.success(this.translate.instant(this.bulkActionStartedMessage)); | ||
this.resetBulkSelection.emit(); | ||
}), | ||
untilDestroyed(this), | ||
).subscribe(); | ||
} | ||
|
||
onBulkRestart(): void { | ||
this.matDialog | ||
.open(StopOptionsDialogComponent, { data: StopOptionsOperation.Restart }) | ||
.afterClosed() | ||
.pipe( | ||
filter(Boolean), | ||
tap((options: VirtualizationStopParams) => { | ||
this.activeCheckedInstances().forEach((instance) => this.restart(instance.id, options)); | ||
this.snackbar.success(this.translate.instant(this.bulkActionStartedMessage)); | ||
this.resetBulkSelection.emit(); | ||
}), | ||
untilDestroyed(this), | ||
).subscribe(); | ||
} | ||
|
||
private start(instanceId: string): void { | ||
this.api.job('virt.instance.start', [instanceId]) | ||
.pipe(this.errorHandler.catchError(), untilDestroyed(this)) | ||
.subscribe(); | ||
} | ||
|
||
private stop(instanceId: string, options: VirtualizationStopParams): void { | ||
this.api.job('virt.instance.stop', [instanceId, options]) | ||
.pipe(this.errorHandler.catchError(), untilDestroyed(this)) | ||
.subscribe(); | ||
} | ||
|
||
private restart(instanceId: string, options: VirtualizationStopParams): void { | ||
this.api.job('virt.instance.restart', [instanceId, options]) | ||
.pipe(this.errorHandler.catchError(), untilDestroyed(this)) | ||
.subscribe(); | ||
} | ||
} |
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.