Skip to content

Commit

Permalink
NAS-133433 / 25.04 / New virtualization > Add VNC support (#11315)
Browse files Browse the repository at this point in the history
* NAS-133433: Add VNC support

* NAS-133433: Set default vnc_port

* NAS-133433: Extract default VNC port into a constant

* NAS-133433: Replace `enable_vnc` to `vnc_enabled`

* NAS-133433: Fix remarks

* NAS-133433: Move vnc link to tool card

* NAS-133433: Fix remarks

* NAS-133433: Add enable vnc checkbox in the instance edit form
  • Loading branch information
bvasilenko authored Jan 20, 2025
1 parent b219843 commit 15f034f
Show file tree
Hide file tree
Showing 101 changed files with 443 additions and 5 deletions.
4 changes: 4 additions & 0 deletions src/app/interfaces/virtualization.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface VirtualizationInstance {
aliases: VirtualizationAlias;
raw: unknown;
image: VirtualizationImage;
vnc_enabled: boolean;
vnc_port: number | null;
}

export interface VirtualizationAlias {
Expand Down Expand Up @@ -71,6 +73,8 @@ export interface UpdateVirtualizationInstance {
autostart?: boolean;
cpu?: string;
memory?: number;
enable_vnc?: boolean;
vnc_port?: number | null;
}

export type VirtualizationDevice =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@
></ix-checkbox>
</ix-fieldset>

@if (isVmInstanceType) {
<ix-fieldset [title]="'VNC' | translate">

<ix-checkbox
formControlName="enable_vnc"
[label]="'Enable VNC' | translate"
></ix-checkbox>

@if (form.value.enable_vnc) {
<ix-input
formControlName="vnc_port"
type="number"
[label]="'VNC Port' | translate"
[required]="true"
></ix-input>
}
</ix-fieldset>
}

<ix-fieldset [title]="'CPU & Memory' | translate">
<ix-input
formControlName="cpu"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { GiB } from 'app/constants/bytes.constant';
import { fakeSuccessfulJob } from 'app/core/testing/utils/fake-job.utils';
import { mockJob, mockApi } from 'app/core/testing/utils/mock-api.utils';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { VirtualizationType } from 'app/enums/virtualization.enum';
import { Job } from 'app/interfaces/job.interface';
import { VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
Expand All @@ -33,6 +34,9 @@ describe('InstanceEditFormComponent', () => {
cpu: '1-3',
memory: 2 * GiB,
environment: {},
type: VirtualizationType.Vm,
vnc_enabled: true,
vnc_port: 9001,
} as VirtualizationInstance;

const createComponent = createComponentFactory({
Expand All @@ -54,6 +58,8 @@ describe('InstanceEditFormComponent', () => {
cpu: '2-5',
memory: GiB,
environment: {},
enable_vnc: true,
vnc_port: 9000,
},
})),
),
Expand All @@ -78,6 +84,7 @@ describe('InstanceEditFormComponent', () => {
Autostart: false,
'CPU Configuration': '1-3',
'Memory Size': '2 GiB',
'VNC Port': '9001',
});
});

Expand All @@ -86,6 +93,7 @@ describe('InstanceEditFormComponent', () => {
Autostart: true,
'CPU Configuration': '2-5',
'Memory Size': '1 GiB',
'VNC Port': 9000,
});

const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
Expand All @@ -96,6 +104,8 @@ describe('InstanceEditFormComponent', () => {
cpu: '2-5',
memory: GiB,
environment: {},
enable_vnc: true,
vnc_port: 9000,
}]);
expect(spectator.inject(DialogService).jobDialog).toHaveBeenCalled();
expect(spectator.inject(SnackbarService).success).toHaveBeenCalled();
Expand All @@ -106,6 +116,8 @@ describe('InstanceEditFormComponent', () => {
cpu: '2-5',
memory: GiB,
environment: {},
enable_vnc: true,
vnc_port: 9000,
},
error: false,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { of } from 'rxjs';
import { Role } from 'app/enums/role.enum';
import { VirtualizationType } from 'app/enums/virtualization.enum';
import { containersHelptext } from 'app/helptext/virtualization/containers';
import {
InstanceEnvVariablesFormGroup,
Expand All @@ -30,6 +31,7 @@ import { SlideInRef } from 'app/modules/slide-ins/slide-in-ref';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { TestDirective } from 'app/modules/test-id/test.directive';
import { ApiService } from 'app/modules/websocket/api.service';
import { defaultVncPort } from 'app/pages/virtualization/virtualization.constants';

@UntilDestroy()
@Component({
Expand Down Expand Up @@ -58,10 +60,16 @@ export class InstanceEditFormComponent {
title: string;
editingInstance: VirtualizationInstance;

get isVmInstanceType(): boolean {
return this.editingInstance.type === VirtualizationType.Vm;
}

protected readonly form = this.formBuilder.nonNullable.group({
autostart: [false],
cpu: ['', [cpuValidator()]],
memory: [null as number | null],
enable_vnc: [false],
vnc_port: [defaultVncPort, [Validators.required, Validators.min(5900), Validators.max(65535)]],
environmentVariables: new FormArray<InstanceEnvVariablesFormGroup>([]),
});

Expand All @@ -79,12 +87,23 @@ export class InstanceEditFormComponent {
return of(this.form.dirty);
});

this.form.controls.vnc_port.disable();
this.form.controls.enable_vnc.valueChanges.pipe(untilDestroyed(this)).subscribe((vncEnabled) => {
if (vncEnabled) {
this.form.controls.vnc_port.enable();
} else {
this.form.controls.vnc_port.disable();
}
});

this.editingInstance = this.slideInRef.getData();
this.title = this.translate.instant('Edit Instance: {name}', { name: this.editingInstance.name });
this.form.patchValue({
cpu: this.editingInstance.cpu,
autostart: this.editingInstance.autostart,
memory: this.editingInstance.memory,
enable_vnc: this.editingInstance.vnc_enabled,
vnc_port: this.editingInstance.vnc_port,
});

Object.keys(this.editingInstance.environment || {}).forEach((key) => {
Expand Down Expand Up @@ -133,6 +152,8 @@ export class InstanceEditFormComponent {
autostart: values.autostart,
cpu: values.cpu,
memory: values.memory,
enable_vnc: values.enable_vnc,
vnc_port: values.enable_vnc ? values.vnc_port || defaultVncPort : null,
} as UpdateVirtualizationInstance;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { VirtualizationInstancesStore } from 'app/pages/virtualization/stores/vi
const instance = {
id: 'demo',
name: 'Demo',
type: VirtualizationType.Container,
type: VirtualizationType.Vm,
status: VirtualizationStatus.Running,
cpu: '525',
autostart: true,
Expand All @@ -48,6 +48,8 @@ const instance = {
},
aliases: {} as VirtualizationAlias,
raw: null,
vnc_enabled: true,
vnc_port: 9000,
} as VirtualizationInstance;

describe('InstanceGeneralInfoComponent', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,29 @@ <h3 mat-card-title>
color="default"
ixTest="open-shell"
class="tool"
[class.disabled]="isInstanceStopped()"
[disabled]="isInstanceStopped()"
[routerLink]="['/virtualization', 'view', instance().id, 'shell']"
>
<span>{{ 'Shell' | translate }}</span>

<ix-icon name="mdi-console"></ix-icon>
</a>
@if (isVm() && instance().vnc_enabled && instance().vnc_port) {
<a
mat-flat-button
color="default"
ixTest="open-vnc"
class="tool"
target="_blank"
[class.disabled]="isInstanceStopped()"
[disabled]="isInstanceStopped()"
[href]="vncLink()"
>
<span>{{ 'VNC' | translate }}</span>
<span>{{ vncLink() }}</span>
</a>
}
</div>
</mat-card-content>
</mat-card>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
display: flex;
margin: 0 -16px;

&:not(&.disabled) {
color: var(--fg2) !important;
}

&.disabled {
color: var(--mdc-filled-button-disabled-label-text-color);
}

::ng-deep .mdc-button__label {
flex-grow: 1;
justify-content: space-between;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { VirtualizationStatus } from 'app/enums/virtualization.enum';
import { mockWindow } from 'app/core/testing/utils/mock-window.utils';
import { VirtualizationStatus, VirtualizationType } from 'app/enums/virtualization.enum';
import { VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import {
InstanceToolsComponent,
Expand All @@ -13,6 +14,14 @@ describe('InstanceToolsComponent', () => {
let loader: HarnessLoader;
const createComponent = createComponentFactory({
component: InstanceToolsComponent,
providers: [
mockWindow({
location: {
hostname: 'truenas.com',
},
open: jest.fn(),
}),
],
});

beforeEach(() => {
Expand All @@ -21,6 +30,9 @@ describe('InstanceToolsComponent', () => {
instance: {
id: 'my-instance',
status: VirtualizationStatus.Running,
type: VirtualizationType.Vm,
vnc_enabled: true,
vnc_port: 5900,
} as VirtualizationInstance,
},
});
Expand All @@ -46,4 +58,39 @@ describe('InstanceToolsComponent', () => {
expect(await shellLink.isDisabled()).toBe(true);
});
});

describe('vnc', () => {
it('shows a link to VNC', async () => {
const vncLink = await loader.getHarness(MatButtonHarness.with({ selector: '[ixTest="open-vnc"]' }));
expect(vncLink).toBeTruthy();

expect(await (await vncLink.host()).getAttribute('href')).toBe('vnc://truenas.com:5900');
});

it('shows vnc link as disabled when instance is not running', async () => {
spectator.setInput('instance', {
id: 'my-instance',
status: VirtualizationStatus.Stopped,
type: VirtualizationType.Vm,
vnc_enabled: true,
vnc_port: 5900,
} as VirtualizationInstance);

const vncLink = await loader.getHarness(MatButtonHarness.with({ selector: '[ixTest="open-vnc"]' }));
expect(await vncLink.isDisabled()).toBe(true);
});

it('hides vnc link when vnc is not enabled', async () => {
spectator.setInput('instance', {
id: 'my-instance',
status: VirtualizationStatus.Stopped,
type: VirtualizationType.Vm,
vnc_enabled: false,
vnc_port: 5900,
} as VirtualizationInstance);

const vncLink = await loader.getHarnessOrNull(MatButtonHarness.with({ selector: '[ixTest="open-vnc"]' }));
expect(vncLink).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
ChangeDetectionStrategy, Component, computed, input,
ChangeDetectionStrategy, Component, computed, Inject, input,
} from '@angular/core';
import { MatAnchor } from '@angular/material/button';
import {
Expand All @@ -8,7 +8,8 @@ import {
import { MatTooltip } from '@angular/material/tooltip';
import { RouterLink } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { VirtualizationStatus } from 'app/enums/virtualization.enum';
import { VirtualizationStatus, VirtualizationType } from 'app/enums/virtualization.enum';
import { WINDOW } from 'app/helpers/window.helper';
import { VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component';
import { TestDirective } from 'app/modules/test-id/test.directive';
Expand Down Expand Up @@ -36,4 +37,10 @@ export class InstanceToolsComponent {
readonly instance = input.required<VirtualizationInstance>();

protected readonly isInstanceStopped = computed(() => this.instance().status !== VirtualizationStatus.Running);
protected readonly isVm = computed(() => this.instance().type === VirtualizationType.Vm);
protected readonly vncLink = computed(() => `vnc://${this.window.location.hostname}:${this.instance().vnc_port}`);

constructor(
@Inject(WINDOW) private window: Window,
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
(click)="onBrowseImages()"
>{{ 'Browse Catalog' | translate }}</button>
</div>

</ix-form-section>

<ix-form-section
Expand Down Expand Up @@ -262,6 +263,24 @@
}

@if (isVm()) {
<ix-form-section
[label]="'VNC' | translate"
[help]="'VNC' | translate"
>
<ix-checkbox
formControlName="enable_vnc"
[label]="'Enable VNC' | translate"
></ix-checkbox>

@if (form.value.enable_vnc) {
<ix-input
formControlName="vnc_port"
type="number"
[label]="'VNC Port' | translate"
></ix-input>
}
</ix-form-section>

<ix-form-section label="TPM">
<ix-checkbox
formControlName="tpm"
Expand Down
Loading

0 comments on commit 15f034f

Please sign in to comment.