-
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-132542 / 25.04 / Allow devices to be added to containers (#11046)
- Loading branch information
Showing
111 changed files
with
1,516 additions
and
41 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
40 changes: 40 additions & 0 deletions
40
...nstances/instance-details/instance-devices/add-device-menu/add-device-menu.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,40 @@ | ||
@if (isLoadingDevices()) { | ||
<ngx-skeleton-loader class="loader"></ngx-skeleton-loader> | ||
} @else if (hasDevicesToAdd()) { | ||
<button | ||
mat-button | ||
ixTest="add-device" | ||
[disabled]="" | ||
[matMenuTriggerFor]="addDeviceMenu" | ||
> | ||
{{ 'Add' | translate }} | ||
</button> | ||
|
||
<mat-menu #addDeviceMenu="matMenu"> | ||
@if (availableUsbDevices().length) { | ||
<h4 class="menu-header">{{ 'USB Devices' | translate }}</h4> | ||
@for (usb of availableUsbDevices(); track usb.product_id) { | ||
<button | ||
mat-menu-item | ||
[ixTest]="['add-usb-device', usb.product]" | ||
(click)="addUsb(usb)" | ||
> | ||
{{ usb.product }} | ||
</button> | ||
} | ||
} | ||
|
||
@if (availableGpuDevices().length) { | ||
<h4 class="menu-header">{{ 'GPUs' | translate }}</h4> | ||
@for (gpu of availableGpuDevices(); track gpu) { | ||
<button | ||
mat-menu-item | ||
[ixTest]="['add-gpu-device', gpu.description]" | ||
(click)="addGpu(gpu)" | ||
> | ||
{{ gpu.description }} | ||
</button> | ||
} | ||
} | ||
</mat-menu> | ||
} |
18 changes: 18 additions & 0 deletions
18
...nstances/instance-details/instance-devices/add-device-menu/add-device-menu.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,18 @@ | ||
.menu-header { | ||
border-bottom: 1px solid var(--lines); | ||
font-size: 12px; | ||
margin-top: 12px; | ||
padding-bottom: 6px; | ||
padding-left: 15px; | ||
} | ||
|
||
:host .loader { | ||
display: block; | ||
flex: unset; | ||
width: 80px; | ||
|
||
::ng-deep .loader { | ||
margin-bottom: -6px; | ||
} | ||
} | ||
|
103 changes: 103 additions & 0 deletions
103
...ances/instance-details/instance-devices/add-device-menu/add-device-menu.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,103 @@ | ||
import { HarnessLoader } from '@angular/cdk/testing'; | ||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; | ||
import { MatMenuHarness } from '@angular/material/menu/testing'; | ||
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; | ||
import { mockApi, mockCall } from 'app/core/testing/utils/mock-api.utils'; | ||
import { VirtualizationDeviceType } from 'app/enums/virtualization.enum'; | ||
import { AvailableGpu, AvailableUsb, VirtualizationDevice } from 'app/interfaces/virtualization.interface'; | ||
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service'; | ||
import { | ||
AddDeviceMenuComponent, | ||
} from 'app/pages/virtualization/components/all-instances/instance-details/instance-devices/add-device-menu/add-device-menu.component'; | ||
import { VirtualizationInstancesStore } from 'app/pages/virtualization/stores/virtualization-instances.store'; | ||
import { ApiService } from 'app/services/api.service'; | ||
|
||
describe('AddDeviceMenuComponent', () => { | ||
let spectator: Spectator<AddDeviceMenuComponent>; | ||
let loader: HarnessLoader; | ||
const createComponent = createComponentFactory({ | ||
component: AddDeviceMenuComponent, | ||
providers: [ | ||
mockApi([ | ||
mockCall('virt.device.usb_choices', { | ||
usb1: { | ||
product_id: 'already-added', | ||
product: 'Web Cam', | ||
} as AvailableUsb, | ||
usb2: { | ||
product_id: 'new', | ||
product: 'Card Reader', | ||
} as AvailableUsb, | ||
}), | ||
mockCall('virt.device.gpu_choices', { | ||
gpu1: { | ||
description: 'NDIVIA XTR 2000', | ||
} as AvailableGpu, | ||
gpu2: { | ||
description: 'MAD Galeon 5000', | ||
} as AvailableGpu, | ||
}), | ||
mockCall('virt.instance.device_add'), | ||
]), | ||
mockProvider(VirtualizationInstancesStore, { | ||
selectedInstance: () => ({ id: 'my-instance' }), | ||
selectedInstanceDevices: () => [ | ||
{ | ||
dev_type: VirtualizationDeviceType.Usb, | ||
product_id: 'already-added', | ||
}, | ||
{ | ||
dev_type: VirtualizationDeviceType.Gpu, | ||
description: 'NDIVIA XTR 2000', | ||
}, | ||
] as VirtualizationDevice[], | ||
loadDevices: jest.fn(), | ||
isLoadingDevices: () => false, | ||
}), | ||
mockProvider(SnackbarService), | ||
], | ||
}); | ||
|
||
beforeEach(() => { | ||
spectator = createComponent(); | ||
loader = TestbedHarnessEnvironment.loader(spectator.fixture); | ||
}); | ||
|
||
it('shows available USB devices and GPUs that have not been already added to this system', async () => { | ||
const menu = await loader.getHarness(MatMenuHarness.with({ triggerText: 'Add' })); | ||
await menu.open(); | ||
|
||
const menuItems = await menu.getItems(); | ||
expect(menuItems).toHaveLength(2); | ||
expect(await menuItems[0].getText()).toContain('Card Reader'); | ||
expect(await menuItems[1].getText()).toContain('MAD Galeon 5000'); | ||
}); | ||
|
||
it('adds a usb device when it is selected', async () => { | ||
const menu = await loader.getHarness(MatMenuHarness.with({ triggerText: 'Add' })); | ||
await menu.open(); | ||
|
||
await menu.clickItem({ text: 'Card Reader' }); | ||
|
||
expect(spectator.inject(ApiService).call).toHaveBeenCalledWith('virt.instance.device_add', ['my-instance', { | ||
dev_type: VirtualizationDeviceType.Usb, | ||
product_id: 'new', | ||
} as VirtualizationDevice]); | ||
expect(spectator.inject(VirtualizationInstancesStore).loadDevices).toHaveBeenCalled(); | ||
expect(spectator.inject(SnackbarService).success).toHaveBeenCalledWith('Device was added'); | ||
}); | ||
|
||
it('adds a gpu when it is selected', async () => { | ||
const menu = await loader.getHarness(MatMenuHarness.with({ triggerText: 'Add' })); | ||
await menu.open(); | ||
|
||
await menu.clickItem({ text: 'MAD Galeon 5000' }); | ||
|
||
expect(spectator.inject(ApiService).call).toHaveBeenCalledWith('virt.instance.device_add', ['my-instance', { | ||
dev_type: VirtualizationDeviceType.Gpu, | ||
description: 'MAD Galeon 5000', | ||
} as VirtualizationDevice]); | ||
expect(spectator.inject(VirtualizationInstancesStore).loadDevices).toHaveBeenCalled(); | ||
expect(spectator.inject(SnackbarService).success).toHaveBeenCalledWith('Device was added'); | ||
}); | ||
}); |
109 changes: 109 additions & 0 deletions
109
...-instances/instance-details/instance-devices/add-device-menu/add-device-menu.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,109 @@ | ||
import { ChangeDetectionStrategy, Component, computed } from '@angular/core'; | ||
import { toSignal } from '@angular/core/rxjs-interop'; | ||
import { MatButton } from '@angular/material/button'; | ||
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; | ||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; | ||
import { TranslateModule, TranslateService } from '@ngx-translate/core'; | ||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; | ||
import { VirtualizationDeviceType, VirtualizationGpuType, VirtualizationType } from 'app/enums/virtualization.enum'; | ||
import { | ||
AvailableGpu, | ||
AvailableUsb, | ||
VirtualizationDevice, | ||
VirtualizationGpu, | ||
VirtualizationUsb, | ||
} from 'app/interfaces/virtualization.interface'; | ||
import { AppLoaderService } from 'app/modules/loader/app-loader.service'; | ||
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service'; | ||
import { TestDirective } from 'app/modules/test-id/test.directive'; | ||
import { VirtualizationInstancesStore } from 'app/pages/virtualization/stores/virtualization-instances.store'; | ||
import { ApiService } from 'app/services/api.service'; | ||
import { ErrorHandlerService } from 'app/services/error-handler.service'; | ||
|
||
@UntilDestroy() | ||
@Component({ | ||
selector: 'ix-add-device-menu', | ||
templateUrl: './add-device-menu.component.html', | ||
styleUrls: ['./add-device-menu.component.scss'], | ||
standalone: true, | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
imports: [ | ||
MatButton, | ||
MatMenu, | ||
MatMenuItem, | ||
TestDirective, | ||
TranslateModule, | ||
MatMenuTrigger, | ||
NgxSkeletonLoaderModule, | ||
], | ||
}) | ||
export class AddDeviceMenuComponent { | ||
private readonly usbChoices = toSignal(this.api.call('virt.device.usb_choices'), { initialValue: {} }); | ||
// TODO: Stop hardcoding params | ||
private readonly gpuChoices = toSignal(this.api.call('virt.device.gpu_choices', [VirtualizationType.Container, VirtualizationGpuType.Physical]), { initialValue: {} }); | ||
|
||
protected readonly isLoadingDevices = this.instanceStore.isLoadingDevices; | ||
|
||
protected readonly availableUsbDevices = computed(() => { | ||
const usbChoices = Object.values(this.usbChoices()); | ||
const existingUsbDevices = this.instanceStore.selectedInstanceDevices() | ||
.filter((device) => device.dev_type === VirtualizationDeviceType.Usb); | ||
|
||
return usbChoices.filter((usb) => { | ||
return !existingUsbDevices.find((device) => device.product_id === usb.product_id); | ||
}); | ||
}); | ||
|
||
protected readonly availableGpuDevices = computed(() => { | ||
const gpuChoices = Object.values(this.gpuChoices()); | ||
const existingGpuDevices = this.instanceStore.selectedInstanceDevices() | ||
.filter((device) => device.dev_type === VirtualizationDeviceType.Gpu); | ||
|
||
return gpuChoices.filter((gpu) => { | ||
// TODO: Condition is incorrect. | ||
return !existingGpuDevices.find((device) => device.description === gpu.description); | ||
}); | ||
}); | ||
|
||
protected readonly hasDevicesToAdd = computed(() => { | ||
return this.availableUsbDevices().length > 0 || this.availableGpuDevices().length > 0; | ||
}); | ||
|
||
constructor( | ||
private instanceStore: VirtualizationInstancesStore, | ||
private api: ApiService, | ||
private errorHandler: ErrorHandlerService, | ||
private loader: AppLoaderService, | ||
private snackbar: SnackbarService, | ||
private translate: TranslateService, | ||
) {} | ||
|
||
protected addUsb(usb: AvailableUsb): void { | ||
this.addDevice({ | ||
dev_type: VirtualizationDeviceType.Usb, | ||
product_id: usb.product_id, | ||
} as VirtualizationUsb); | ||
} | ||
|
||
protected addGpu(gpu: AvailableGpu): void { | ||
this.addDevice({ | ||
dev_type: VirtualizationDeviceType.Gpu, | ||
// TODO: Incorrect value. | ||
description: gpu.description, | ||
} as VirtualizationGpu); | ||
} | ||
|
||
private addDevice(payload: VirtualizationDevice): void { | ||
const instanceId = this.instanceStore.selectedInstance().id; | ||
this.api.call('virt.instance.device_add', [instanceId, payload]) | ||
.pipe( | ||
this.loader.withLoader(), | ||
this.errorHandler.catchError(), | ||
untilDestroyed(this), | ||
) | ||
.subscribe(() => { | ||
this.snackbar.success(this.translate.instant('Device was added')); | ||
this.instanceStore.loadDevices(); | ||
}); | ||
} | ||
} |
Oops, something went wrong.