diff --git a/src/app/enums/virtualization.enum.ts b/src/app/enums/virtualization.enum.ts index d1fefac781b..7142b90fd78 100644 --- a/src/app/enums/virtualization.enum.ts +++ b/src/app/enums/virtualization.enum.ts @@ -55,9 +55,9 @@ export const virtualizationDeviceTypeLabels = new Map - + @if (form.controls.destination.enabled) { + + } diff --git a/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disk-form/instance-disk-form.component.ts b/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disk-form/instance-disk-form.component.ts index 8dc1092f4a9..d30d25e1922 100644 --- a/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disk-form/instance-disk-form.component.ts +++ b/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disk-form/instance-disk-form.component.ts @@ -7,8 +7,8 @@ import { MatCard, MatCardContent } from '@angular/material/card'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { Observable, of } from 'rxjs'; -import { VirtualizationDeviceType } from 'app/enums/virtualization.enum'; -import { VirtualizationDisk } from 'app/interfaces/virtualization.interface'; +import { VirtualizationDeviceType, VirtualizationType } from 'app/enums/virtualization.enum'; +import { VirtualizationDisk, VirtualizationInstance } from 'app/interfaces/virtualization.interface'; import { FormActionsComponent } from 'app/modules/forms/ix-forms/components/form-actions/form-actions.component'; import { IxExplorerComponent } from 'app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component'; import { IxFieldsetComponent } from 'app/modules/forms/ix-forms/components/ix-fieldset/ix-fieldset.component'; @@ -22,7 +22,7 @@ import { ApiService } from 'app/modules/websocket/api.service'; import { FilesystemService } from 'app/services/filesystem.service'; interface InstanceDiskFormOptions { - instanceId: string; + instance: VirtualizationInstance; disk: VirtualizationDisk | undefined; } @@ -50,7 +50,7 @@ export class InstanceDiskFormComponent implements OnInit { private existingDisk = signal(null); protected readonly isLoading = signal(false); - protected readonly directoryNodeProvider = this.filesystem.getFilesystemNodeProvider({ directoriesOnly: false }); + protected readonly directoryNodeProvider = this.filesystem.getFilesystemNodeProvider({ datasetsAndZvols: true }); protected form = this.formBuilder.nonNullable.group({ source: ['', Validators.required], @@ -63,6 +63,8 @@ export class InstanceDiskFormComponent implements OnInit { return !this.isNew() ? this.translate.instant('Edit Disk') : this.translate.instant('Add Disk'); }); + protected instance = computed(() => this.slideInRef.getData().instance); + constructor( private formBuilder: FormBuilder, private errorHandler: FormErrorHandlerService, @@ -86,6 +88,9 @@ export class InstanceDiskFormComponent implements OnInit { destination: disk.destination || '', }); } + if (this.instance().type === VirtualizationType.Vm) { + this.form.controls.destination.disable(); + } } onSubmit(): void { @@ -112,7 +117,7 @@ export class InstanceDiskFormComponent implements OnInit { } private prepareRequest(): Observable { - const instanceId = this.slideInRef.getData().instanceId; + const instanceId = this.slideInRef.getData().instance.id; const payload = { ...this.form.value, diff --git a/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disks.component.html b/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disks.component.html index 570e37eab51..ba12e3b92ff 100644 --- a/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disks.component.html +++ b/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disks.component.html @@ -17,11 +17,11 @@

} @else { @for (disk of visibleDisks(); track disk.name) {
-
- {{ disk.source }} - → - {{ disk.destination }} -
+ @if (disk.destination) { +
{{ disk.source }} → {{ disk.destination }}
+ } @else { +
{{ disk.source }}
+ } { providers: [ mockProvider(VirtualizationDevicesStore, { isLoading: () => false, - selectedInstance: () => ({ id: 'my-instance' }), + selectedInstance: () => ({ id: 'my-instance', type: VirtualizationType.Container }), devices: () => disks, loadDevices: jest.fn(), }), @@ -65,7 +65,7 @@ describe('InstanceDisksComponent', () => { const diskRows = spectator.queryAll('.disk'); expect(diskRows).toHaveLength(1); - expect(diskRows[0]).toHaveText('source-path → destination'); + expect(diskRows[0]).toHaveText('/mnt/source-path → destination'); }); it('renders a menu to manage the disk', () => { @@ -74,23 +74,51 @@ describe('InstanceDisksComponent', () => { expect(actionsMenu[0].device).toBe(disks[0]); }); - it('opens disk form when Add is pressed', async () => { - const addButton = await loader.getHarness(MatButtonHarness.with({ text: 'Add' })); - await addButton.click(); + describe('container', () => { + it('opens disk form when Add is pressed', async () => { + const addButton = await loader.getHarness(MatButtonHarness.with({ text: 'Add' })); + await addButton.click(); - expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith( - InstanceDiskFormComponent, - { data: { disk: undefined, instanceId: 'my-instance' } }, - ); + expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith( + InstanceDiskFormComponent, + { data: { disk: undefined, instance: { id: 'my-instance', type: VirtualizationType.Container } as VirtualizationInstance } }, + ); + }); + + it('opens disk for for edit when actions menu emits (edit)', () => { + const actionsMenu = spectator.query(DeviceActionsMenuComponent)!; + actionsMenu.edit.emit(); + + expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith( + InstanceDiskFormComponent, + { data: { disk: disks[0], instance: { id: 'my-instance', type: VirtualizationType.Container } as VirtualizationInstance } }, + ); + }); }); + describe('vm', () => { + it('opens disk form when Add is pressed', async () => { + jest.spyOn(spectator.inject(VirtualizationDevicesStore), 'selectedInstance') + .mockReturnValue({ id: 'my-instance', type: VirtualizationType.Vm } as VirtualizationInstance); + + const addButton = await loader.getHarness(MatButtonHarness.with({ text: 'Add' })); + await addButton.click(); + + expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith( + InstanceDiskFormComponent, + { data: { disk: undefined, instance: { id: 'my-instance', type: VirtualizationType.Vm } as VirtualizationInstance } }, + ); + }); - it('opens disk for for edit when actions menu emits (edit)', () => { - const actionsMenu = spectator.query(DeviceActionsMenuComponent)!; - actionsMenu.edit.emit(); + it('opens disk for for edit when actions menu emits (edit)', () => { + jest.spyOn(spectator.inject(VirtualizationDevicesStore), 'selectedInstance') + .mockReturnValue({ id: 'my-instance', type: VirtualizationType.Vm } as VirtualizationInstance); + const actionsMenu = spectator.query(DeviceActionsMenuComponent)!; + actionsMenu.edit.emit(); - expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith( - InstanceDiskFormComponent, - { data: { disk: disks[0], instanceId: 'my-instance' } }, - ); + expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith( + InstanceDiskFormComponent, + { data: { disk: disks[0], instance: { id: 'my-instance', type: VirtualizationType.Vm } as VirtualizationInstance } }, + ); + }); }); }); diff --git a/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disks.component.ts b/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disks.component.ts index 9e457fd795f..d5ab6ab0c74 100644 --- a/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disks.component.ts +++ b/src/app/pages/virtualization/components/all-instances/instance-details/instance-disks/instance-disks.component.ts @@ -60,7 +60,7 @@ export class InstanceDisksComponent { } private openDiskForm(disk?: VirtualizationDisk): void { - this.slideIn.open(InstanceDiskFormComponent, { data: { disk, instanceId: this.deviceStore.selectedInstance().id } }) + this.slideIn.open(InstanceDiskFormComponent, { data: { disk, instance: this.deviceStore.selectedInstance() } }) .pipe(filter((result) => !!result.response), untilDestroyed(this)) .subscribe(() => this.deviceStore.loadDevices()); } diff --git a/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.html b/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.html index 3bee549d24d..f8e048c56b9 100644 --- a/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.html +++ b/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.html @@ -49,7 +49,8 @@ + @if (isContainer()) { + } - + @if (isContainer()) { + + } } diff --git a/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.spec.ts b/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.spec.ts index 6c1c75763f7..68a5452d421 100644 --- a/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.spec.ts +++ b/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.spec.ts @@ -116,209 +116,215 @@ describe('InstanceWizardComponent', () => { form = await loader.getHarness(IxFormHarness); }); - it('opens SelectImageDialogComponent when Browse image button is pressed and show image label when image is selected', async () => { - const browseButton = await loader.getHarness(MatButtonHarness.with({ text: 'Browse Catalog' })); - await browseButton.click(); - - expect(spectator.inject(MatDialog).open).toHaveBeenCalled(); - expect(await form.getValues()).toMatchObject({ - Image: 'almalinux/8/cloud', - }); - }); - - it('creates new container instance when form is submitted', async () => { - await form.fillForm({ - Name: 'new', - 'CPU Configuration': '1-2', - 'Memory Size': '1 GiB', - }); - - const browseButton = await loader.getHarness(MatButtonHarness.with({ text: 'Browse Catalog' })); - await browseButton.click(); - - const diskList = await loader.getHarness(IxListHarness.with({ label: 'Disks' })); - await diskList.pressAddButton(); - const diskForm = await diskList.getLastListItem(); - await diskForm.fillForm({ - Source: '/mnt/source', - Destination: 'destination', - }); - - const proxiesList = await loader.getHarness(IxListHarness.with({ label: 'Proxies' })); - await proxiesList.pressAddButton(); - const proxyForm = await proxiesList.getLastListItem(); - await proxyForm.fillForm({ - 'Host Port': 3000, - 'Host Protocol': 'TCP', - 'Instance Port': 2000, - 'Instance Protocol': 'UDP', - }); - - // TODO: Fix this to use IxCheckboxHarness - const usbDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ - label: 'xHCI Host Controller (0003)', - })); - await usbDeviceCheckbox.check(); - - const useDefaultNetworkCheckbox = await loader.getHarness(IxCheckboxHarness.with({ label: 'Use default network settings' })); - await useDefaultNetworkCheckbox.setValue(false); - - // TODO: Fix this to use IxCheckboxHarness - const nicDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ label: 'nic1' })); - await nicDeviceCheckbox.check(); - - // TODO: Fix this to use IxCheckboxHarness - const gpuDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ label: 'NVIDIA GeForce GTX 1080' })); - await gpuDeviceCheckbox.check(); - - const createButton = await loader.getHarness(MatButtonHarness.with({ text: 'Create' })); - await createButton.click(); - - expect(spectator.inject(ApiService).job).toHaveBeenCalledWith('virt.instance.create', [{ - name: 'new', - autostart: true, - cpu: '1-2', - instance_type: VirtualizationType.Container, - devices: [ - { - dev_type: VirtualizationDeviceType.Disk, - source: '/mnt/source', - destination: 'destination', - }, - { - dev_type: VirtualizationDeviceType.Proxy, - source_port: 3000, - source_proto: VirtualizationProxyProtocol.Tcp, - dest_port: 2000, - dest_proto: VirtualizationProxyProtocol.Udp, - }, - { dev_type: VirtualizationDeviceType.Nic, nic_type: VirtualizationNicType.Bridged, parent: 'nic1' }, - { dev_type: VirtualizationDeviceType.Usb, product_id: '0003' }, - { dev_type: VirtualizationDeviceType.Gpu, pci: 'pci_0000_01_00_0' }, - ], - image: 'almalinux/8/cloud', - memory: GiB, - environment: {}, - }]); - expect(spectator.inject(DialogService).jobDialog).toHaveBeenCalled(); - expect(spectator.inject(SnackbarService).success).toHaveBeenCalled(); - }); - - it('creates new vm instance when form is submitted', async () => { - await form.fillForm({ - Name: 'new', - 'CPU Configuration': '1-2', - 'Memory Size': '1 GiB', - }); - - const instanceType = await loader.getHarness(IxIconGroupHarness.with({ label: 'Virtualization Method' })); - await instanceType.setValue('VM'); - - const browseButton = await loader.getHarness(MatButtonHarness.with({ text: 'Browse Catalog' })); - await browseButton.click(); - - const diskList = await loader.getHarness(IxListHarness.with({ label: 'Disks' })); - await diskList.pressAddButton(); - const diskForm = await diskList.getLastListItem(); - await diskForm.fillForm({ - Source: '/mnt/source', - Destination: 'destination', + describe('container', () => { + it('creates new instance when form is submitted', async () => { + await form.fillForm({ + Name: 'new', + 'CPU Configuration': '1-2', + 'Memory Size': '1 GiB', + }); + + const browseButton = await loader.getHarness(MatButtonHarness.with({ text: 'Browse Catalog' })); + await browseButton.click(); + + expect(spectator.inject(MatDialog).open).toHaveBeenCalled(); + expect(await form.getValues()).toMatchObject({ + Image: 'almalinux/8/cloud', + }); + + const diskList = await loader.getHarness(IxListHarness.with({ label: 'Disks' })); + await diskList.pressAddButton(); + const diskForm = await diskList.getLastListItem(); + await diskForm.fillForm({ + Source: '/mnt/source', + Destination: 'destination', + }); + + const proxiesList = await loader.getHarness(IxListHarness.with({ label: 'Proxies' })); + await proxiesList.pressAddButton(); + const proxyForm = await proxiesList.getLastListItem(); + await proxyForm.fillForm({ + 'Host Port': 3000, + 'Host Protocol': 'TCP', + 'Instance Port': 2000, + 'Instance Protocol': 'UDP', + }); + + // TODO: Fix this to use IxCheckboxHarness + const usbDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ + label: 'xHCI Host Controller (0003)', + })); + await usbDeviceCheckbox.check(); + + const useDefaultNetworkCheckbox = await loader.getHarness(IxCheckboxHarness.with({ label: 'Use default network settings' })); + await useDefaultNetworkCheckbox.setValue(false); + + // TODO: Fix this to use IxCheckboxHarness + const nicDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ label: 'nic1' })); + await nicDeviceCheckbox.check(); + + // TODO: Fix this to use IxCheckboxHarness + const gpuDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ label: 'NVIDIA GeForce GTX 1080' })); + await gpuDeviceCheckbox.check(); + + const createButton = await loader.getHarness(MatButtonHarness.with({ text: 'Create' })); + await createButton.click(); + + expect(spectator.inject(ApiService).job).toHaveBeenCalledWith('virt.instance.create', [{ + name: 'new', + autostart: true, + cpu: '1-2', + instance_type: VirtualizationType.Container, + devices: [ + { + dev_type: VirtualizationDeviceType.Disk, + source: '/mnt/source', + destination: 'destination', + }, + { + dev_type: VirtualizationDeviceType.Proxy, + source_port: 3000, + source_proto: VirtualizationProxyProtocol.Tcp, + dest_port: 2000, + dest_proto: VirtualizationProxyProtocol.Udp, + }, + { dev_type: VirtualizationDeviceType.Nic, nic_type: VirtualizationNicType.Bridged, parent: 'nic1' }, + { dev_type: VirtualizationDeviceType.Usb, product_id: '0003' }, + { dev_type: VirtualizationDeviceType.Gpu, pci: 'pci_0000_01_00_0' }, + ], + image: 'almalinux/8/cloud', + memory: GiB, + environment: {}, + }]); + expect(spectator.inject(DialogService).jobDialog).toHaveBeenCalled(); + expect(spectator.inject(SnackbarService).success).toHaveBeenCalled(); }); - const proxiesList = await loader.getHarness(IxListHarness.with({ label: 'Proxies' })); - await proxiesList.pressAddButton(); - const proxyForm = await proxiesList.getLastListItem(); - await proxyForm.fillForm({ - 'Host Port': 3000, - 'Host Protocol': 'TCP', - 'Instance Port': 2000, - 'Instance Protocol': 'UDP', + it('sends no NIC devices when default network settings checkbox is set', async () => { + await form.fillForm({ + Name: 'new', + 'CPU Configuration': '1-2', + 'Memory Size': '1 GiB', + }); + + const browseButton = await loader.getHarness(MatButtonHarness.with({ text: 'Browse Catalog' })); + await browseButton.click(); + + expect(spectator.inject(MatDialog).open).toHaveBeenCalled(); + expect(await form.getValues()).toMatchObject({ + Image: 'almalinux/8/cloud', + }); + + const useDefaultNetworkCheckbox = await loader.getHarness(IxCheckboxHarness.with({ label: 'Use default network settings' })); + await useDefaultNetworkCheckbox.setValue(false); + + // TODO: Fix this to use IxCheckboxHarness + const nicDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ label: 'nic1' })); + await nicDeviceCheckbox.check(); + + await useDefaultNetworkCheckbox.setValue(true); // no nic1 should be send now + spectator.detectChanges(); + + const createButton = await loader.getHarness(MatButtonHarness.with({ text: 'Create' })); + await createButton.click(); + + expect(spectator.inject(ApiService).job).toHaveBeenCalledWith('virt.instance.create', [{ + name: 'new', + autostart: true, + cpu: '1-2', + devices: [], + image: 'almalinux/8/cloud', + memory: GiB, + instance_type: 'CONTAINER', + environment: {}, + }]); + expect(spectator.inject(DialogService).jobDialog).toHaveBeenCalled(); + expect(spectator.inject(SnackbarService).success).toHaveBeenCalled(); }); - - // TODO: Fix this to use IxCheckboxHarness - const usbDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ - label: 'xHCI Host Controller (0003)', - })); - await usbDeviceCheckbox.check(); - - const useDefaultNetworkCheckbox = await loader.getHarness(IxCheckboxHarness.with({ label: 'Use default network settings' })); - await useDefaultNetworkCheckbox.setValue(false); - - // TODO: Fix this to use IxCheckboxHarness - const nicDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ label: 'nic1' })); - await nicDeviceCheckbox.check(); - - // TODO: Fix this to use IxCheckboxHarness - const gpuDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ label: 'NVIDIA GeForce GTX 1080' })); - await gpuDeviceCheckbox.check(); - - const createButton = await loader.getHarness(MatButtonHarness.with({ text: 'Create' })); - await createButton.click(); - - expect(spectator.inject(ApiService).job).toHaveBeenCalledWith('virt.instance.create', [{ - name: 'new', - autostart: true, - cpu: '1-2', - instance_type: VirtualizationType.Vm, - devices: [ - { - dev_type: VirtualizationDeviceType.Disk, - source: '/mnt/source', - destination: 'destination', - }, - { - dev_type: VirtualizationDeviceType.Proxy, - source_port: 3000, - source_proto: VirtualizationProxyProtocol.Tcp, - dest_port: 2000, - dest_proto: VirtualizationProxyProtocol.Udp, - }, - { dev_type: VirtualizationDeviceType.Nic, nic_type: VirtualizationNicType.Bridged, parent: 'nic1' }, - { dev_type: VirtualizationDeviceType.Usb, product_id: '0003' }, - { dev_type: VirtualizationDeviceType.Gpu, pci: 'pci_0000_01_00_0' }, - ], - image: 'almalinux/8/cloud', - memory: GiB, - environment: {}, - }]); - expect(spectator.inject(DialogService).jobDialog).toHaveBeenCalled(); - expect(spectator.inject(SnackbarService).success).toHaveBeenCalled(); }); - it('sends no NIC devices when default network settings checkbox is set', async () => { - await form.fillForm({ - Name: 'new', - 'CPU Configuration': '1-2', - 'Memory Size': '1 GiB', + describe('vm', () => { + it('creates new instance when form is submitted', async () => { + await form.fillForm({ + Name: 'new', + 'CPU Configuration': '1-2', + 'Memory Size': '1 GiB', + }); + + const instanceType = await loader.getHarness(IxIconGroupHarness.with({ label: 'Virtualization Method' })); + await instanceType.setValue('VM'); + + const browseButton = await loader.getHarness(MatButtonHarness.with({ text: 'Browse Catalog' })); + await browseButton.click(); + + expect(spectator.inject(MatDialog).open).toHaveBeenCalled(); + expect(await form.getValues()).toMatchObject({ + Image: 'almalinux/8/cloud', + }); + + const diskList = await loader.getHarness(IxListHarness.with({ label: 'Disks' })); + await diskList.pressAddButton(); + const diskForm = await diskList.getLastListItem(); + await diskForm.fillForm({ + Source: '/mnt/source', + }); + + const proxiesList = await loader.getHarness(IxListHarness.with({ label: 'Proxies' })); + await proxiesList.pressAddButton(); + const proxyForm = await proxiesList.getLastListItem(); + await proxyForm.fillForm({ + 'Host Port': 3000, + 'Host Protocol': 'TCP', + 'Instance Port': 2000, + 'Instance Protocol': 'UDP', + }); + + // TODO: Fix this to use IxCheckboxHarness + const usbDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ + label: 'xHCI Host Controller (0003)', + })); + await usbDeviceCheckbox.check(); + + const useDefaultNetworkCheckbox = await loader.getHarness(IxCheckboxHarness.with({ label: 'Use default network settings' })); + await useDefaultNetworkCheckbox.setValue(false); + + // TODO: Fix this to use IxCheckboxHarness + const nicDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ label: 'nic1' })); + await nicDeviceCheckbox.check(); + + // TODO: Fix this to use IxCheckboxHarness + const gpuDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ label: 'NVIDIA GeForce GTX 1080' })); + await gpuDeviceCheckbox.check(); + + const createButton = await loader.getHarness(MatButtonHarness.with({ text: 'Create' })); + await createButton.click(); + + expect(spectator.inject(ApiService).job).toHaveBeenCalledWith('virt.instance.create', [{ + name: 'new', + autostart: true, + cpu: '1-2', + instance_type: VirtualizationType.Vm, + devices: [ + { + dev_type: VirtualizationDeviceType.Disk, + source: '/mnt/source', + }, + { + dev_type: VirtualizationDeviceType.Proxy, + source_port: 3000, + source_proto: VirtualizationProxyProtocol.Tcp, + dest_port: 2000, + dest_proto: VirtualizationProxyProtocol.Udp, + }, + { dev_type: VirtualizationDeviceType.Nic, nic_type: VirtualizationNicType.Bridged, parent: 'nic1' }, + { dev_type: VirtualizationDeviceType.Usb, product_id: '0003' }, + { dev_type: VirtualizationDeviceType.Gpu, pci: 'pci_0000_01_00_0' }, + ], + image: 'almalinux/8/cloud', + memory: GiB, + }]); + expect(spectator.inject(DialogService).jobDialog).toHaveBeenCalled(); + expect(spectator.inject(SnackbarService).success).toHaveBeenCalled(); }); - - const browseButton = await loader.getHarness(MatButtonHarness.with({ text: 'Browse Catalog' })); - await browseButton.click(); - - const useDefaultNetworkCheckbox = await loader.getHarness(IxCheckboxHarness.with({ label: 'Use default network settings' })); - await useDefaultNetworkCheckbox.setValue(false); - - // TODO: Fix this to use IxCheckboxHarness - const nicDeviceCheckbox = await loader.getHarness(MatCheckboxHarness.with({ label: 'nic1' })); - await nicDeviceCheckbox.check(); - - await useDefaultNetworkCheckbox.setValue(true); // no nic1 should be send now - spectator.detectChanges(); - - const createButton = await loader.getHarness(MatButtonHarness.with({ text: 'Create' })); - await createButton.click(); - - expect(spectator.inject(ApiService).job).toHaveBeenCalledWith('virt.instance.create', [{ - name: 'new', - autostart: true, - cpu: '1-2', - devices: [], - image: 'almalinux/8/cloud', - memory: GiB, - environment: {}, - instance_type: 'CONTAINER', - }]); - expect(spectator.inject(DialogService).jobDialog).toHaveBeenCalled(); - expect(spectator.inject(SnackbarService).success).toHaveBeenCalled(); }); }); diff --git a/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.ts b/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.ts index a2ed66c7ff9..a46bf229d7e 100644 --- a/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.ts +++ b/src/app/pages/virtualization/components/instance-wizard/instance-wizard.component.ts @@ -1,6 +1,6 @@ import { AsyncPipe } from '@angular/common'; import { - ChangeDetectionStrategy, Component, signal, + ChangeDetectionStrategy, Component, computed, signal, } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { @@ -94,7 +94,6 @@ import { FilesystemService } from 'app/services/filesystem.service'; export class InstanceWizardComponent { protected readonly isLoading = signal(false); protected readonly requiredRoles = [Role.VirtGlobalWrite]; - protected readonly VirtualizationNicType = VirtualizationNicType; protected readonly virtualizationTypeOptions$ = of(mapToOptions(virtualizationTypeLabels, this.translate)); protected readonly virtualizationTypeIcons = virtualizationTypeIcons; @@ -104,8 +103,6 @@ export class InstanceWizardComponent { protected readonly bridgedNicTypeLabel = virtualizationNicTypeLabels.get(VirtualizationNicType.Bridged); protected readonly macVlanNicTypeLabel = virtualizationNicTypeLabels.get(VirtualizationNicType.Macvlan); - readonly directoryNodeProvider = this.filesystem.getFilesystemNodeProvider(); - bridgedNicDevices$ = this.getNicDevicesOptions(VirtualizationNicType.Bridged); macVlanNicDevices$ = this.getNicDevicesOptions(VirtualizationNicType.Macvlan); @@ -154,6 +151,18 @@ export class InstanceWizardComponent { return this.authService.hasRole(this.requiredRoles); } + protected readonly instanceType = signal(this.form.value.instance_type); + protected readonly isContainer = computed(() => this.instanceType() === VirtualizationType.Container); + protected readonly isVm = computed(() => this.instanceType() === VirtualizationType.Vm); + + readonly directoryNodeProvider = computed(() => { + if (this.isVm()) { + return this.filesystem.getFilesystemNodeProvider({ zvolsOnly: true }); + } + + return this.filesystem.getFilesystemNodeProvider({ datasetsAndZvols: true }); + }); + constructor( private api: ApiService, private formBuilder: FormBuilder, @@ -166,7 +175,18 @@ export class InstanceWizardComponent { protected formatter: IxFormatterService, private authService: AuthService, private filesystem: FilesystemService, - ) {} + ) { + this.form.controls.instance_type.valueChanges.pipe(untilDestroyed(this)).subscribe((type) => { + this.instanceType.set(type); + if (type === VirtualizationType.Container) { + this.form.controls.cpu.setValidators(cpuValidator()); + this.form.controls.memory.clearValidators(); + } else { + this.form.controls.cpu.setValidators([Validators.required, cpuValidator()]); + this.form.controls.memory.setValidators([Validators.required]); + } + }); + } protected onBrowseImages(): void { this.matDialog @@ -205,6 +225,10 @@ export class InstanceWizardComponent { destination: ['', Validators.required], }); + if (this.isVm()) { + control.removeControl('destination'); + } + this.form.controls.disks.push(control); } @@ -255,7 +279,7 @@ export class InstanceWizardComponent { cpu: this.form.controls.cpu.value, memory: this.form.controls.memory.value, image: this.form.controls.image.value, - environment: this.environmentVariablesPayload, + ...(this.isContainer() ? { environment: this.environmentVariablesPayload } : null), } as CreateVirtualizationInstance; } @@ -284,7 +308,7 @@ export class InstanceWizardComponent { const disks = this.form.controls.disks.value.map((proxy) => ({ dev_type: VirtualizationDeviceType.Disk, source: proxy.source, - destination: proxy.destination, + ...(this.isContainer() ? { destination: proxy.destination } : null), })); const usbDevices: { dev_type: VirtualizationDeviceType; product_id: string }[] = []; diff --git a/src/app/services/filesystem.service.ts b/src/app/services/filesystem.service.ts index 229e448be21..d20cbc187fc 100644 --- a/src/app/services/filesystem.service.ts +++ b/src/app/services/filesystem.service.ts @@ -17,7 +17,23 @@ export interface ProviderOptions { showHiddenFiles?: boolean; includeSnapshots?: boolean; datasetsAndZvols?: boolean; + zvolsOnly?: boolean; } + +const roolZvolNode = { + path: '/dev/zvol', + name: '/dev/zvol', + hasChildren: true, + type: ExplorerNodeType.Directory, +} as ExplorerNodeData; + +const roolDatasetNode = { + path: '/mnt', + name: '/mnt', + hasChildren: true, + type: ExplorerNodeType.Directory, +}; + @Injectable({ providedIn: 'root' }) export class FilesystemService { constructor( @@ -33,27 +49,16 @@ export class FilesystemService { showHiddenFiles: false, includeSnapshots: true, datasetsAndZvols: false, + zvolsOnly: false, ...providerOptions, }; return (node: TreeNode) => { - if (options.datasetsAndZvols) { - if (node.data.path.trim() === '/') { - return of([ - { - path: '/mnt', - name: '/mnt', - hasChildren: true, - type: ExplorerNodeType.Directory, - }, - { - path: '/dev/zvol', - name: '/dev/zvol', - hasChildren: true, - type: ExplorerNodeType.Directory, - }, - ] as ExplorerNodeData[]); - } + if (options.datasetsAndZvols && node.data.path.trim() === '/') { + return of([roolDatasetNode, roolZvolNode]); + } + if (options.zvolsOnly && node.data.path.trim() === '/') { + return of([roolZvolNode]); } const typeFilter: [QueryFilter?] = []; if (options.directoriesOnly) { @@ -71,11 +76,10 @@ export class FilesystemService { }; return this.api.call('filesystem.listdir', [node.data.path, typeFilter, queryOptions]).pipe( - map((files) => { const children: ExplorerNodeData[] = []; files.forEach((file) => { - if ((!options.datasetsAndZvols && file.type === FileType.Symlink) || !file.hasOwnProperty('name')) { + if ((!(options.datasetsAndZvols || options.zvolsOnly) && file.type === FileType.Symlink) || !file.hasOwnProperty('name')) { return; }