Skip to content

Commit

Permalink
NAS-134095 / 25.10 / Allow root disk size to be increased (#11532)
Browse files Browse the repository at this point in the history
  • Loading branch information
undsoft authored Feb 12, 2025
1 parent 6df85fc commit b93bfe9
Show file tree
Hide file tree
Showing 97 changed files with 613 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/app/interfaces/virtualization.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface UpdateVirtualizationInstance {
vnc_port?: number | null;
secure_boot?: boolean;
vnc_password?: string | null;
root_disk_size?: number;
}

export type VirtualizationDevice =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ <h3 mat-card-title>
<span class="value">
{{ +instance().root_disk_size | ixFileSize }}
</span>
<a
class="action"
ixTest="increase-root-disk-size"
(click)="showRootDiskIncreaseDialog()"
>
{{ 'Increase' | translate }}
</a>
</div>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
}

.root-disk-size {
display: flex;
margin-bottom: 6px;

.action {
color: var(--primary);
cursor: pointer;
margin-left: auto;
text-decoration: underline;
}
}

.label {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { MatDialog } from '@angular/material/dialog';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
Expand All @@ -18,6 +19,10 @@ import {
DeviceActionsMenuComponent,
} from 'app/pages/instances/components/common/device-actions-menu/device-actions-menu.component';
import { VirtualizationDevicesStore } from 'app/pages/instances/stores/virtualization-devices.store';
import { VirtualizationInstancesStore } from 'app/pages/instances/stores/virtualization-instances.store';
import {
IncreaseRootDiskSizeComponent,
} from 'app/pages/virtualization/components/all-instances/instance-details/instance-disks/increase-root-disk-size/increase-root-disk-size.component';

describe('InstanceDisksComponent', () => {
let spectator: Spectator<InstanceDisksComponent>;
Expand Down Expand Up @@ -49,11 +54,19 @@ describe('InstanceDisksComponent', () => {
devices: () => disks,
loadDevices: jest.fn(),
}),
mockProvider(VirtualizationInstancesStore, {
instanceUpdated: jest.fn(),
}),
mockProvider(SlideIn, {
open: jest.fn(() => of({
response: true,
})),
}),
mockProvider(MatDialog, {
open: jest.fn(() => ({
afterClosed: () => of(true),
})),
}),
],
});

Expand Down Expand Up @@ -100,6 +113,7 @@ describe('InstanceDisksComponent', () => {
);
});
});

describe('vm', () => {
const vm = {
id: 'my-instance',
Expand Down Expand Up @@ -135,5 +149,17 @@ describe('InstanceDisksComponent', () => {

expect(rootDisk).toHaveText('Root Disk: 10 GiB');
});

it('opens dialog to increase root disk size when Increase link is pressed', () => {
const link = spectator.query('.root-disk-size .action');
expect(link).toHaveText('Increase');

spectator.click(link);

expect(spectator.inject(MatDialog).open).toHaveBeenCalledWith(IncreaseRootDiskSizeComponent, {
data: vm,
});
expect(spectator.inject(VirtualizationInstancesStore).instanceUpdated).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { MatButton } from '@angular/material/button';
import {
MatCard, MatCardContent, MatCardHeader, MatCardTitle,
} from '@angular/material/card';
import { MatDialog } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateModule } from '@ngx-translate/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { filter } from 'rxjs/operators';
import { GiB } from 'app/constants/bytes.constant';
import { VirtualizationDeviceType } from 'app/enums/virtualization.enum';
import { VirtualizationDisk, VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import { FileSizePipe } from 'app/modules/pipes/file-size/file-size.pipe';
Expand All @@ -19,6 +21,10 @@ import {
} from 'app/pages/instances/components/all-instances/instance-details/instance-disks/instance-disk-form/instance-disk-form.component';
import { DeviceActionsMenuComponent } from 'app/pages/instances/components/common/device-actions-menu/device-actions-menu.component';
import { VirtualizationDevicesStore } from 'app/pages/instances/stores/virtualization-devices.store';
import { VirtualizationInstancesStore } from 'app/pages/instances/stores/virtualization-instances.store';
import {
IncreaseRootDiskSizeComponent,
} from 'app/pages/virtualization/components/all-instances/instance-details/instance-disks/increase-root-disk-size/increase-root-disk-size.component';

@UntilDestroy()
@Component({
Expand Down Expand Up @@ -47,7 +53,9 @@ export class InstanceDisksComponent {

constructor(
private slideIn: SlideIn,
private matDialog: MatDialog,
private deviceStore: VirtualizationDevicesStore,
private instanceStore: VirtualizationInstancesStore,
) {}

protected readonly visibleDisks = computed(() => {
Expand All @@ -65,6 +73,16 @@ export class InstanceDisksComponent {
this.openDiskForm(disk);
}

protected showRootDiskIncreaseDialog(): void {
this.matDialog.open(IncreaseRootDiskSizeComponent, { data: this.instance() })
.afterClosed()
.pipe(filter(Boolean), untilDestroyed(this))
.subscribe((newRootDiskSize: number) => this.instanceStore.instanceUpdated({
...this.instance(),
root_disk_size: newRootDiskSize * GiB,
}));
}

private openDiskForm(disk?: VirtualizationDisk): void {
this.slideIn.open(InstanceDiskFormComponent, { data: { disk, instance: this.instance() } })
.pipe(filter((result) => !!result.response), untilDestroyed(this))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<h1 matDialogTitle>
{{ 'Increase Root Disk Size' | translate }}
</h1>

<form class="ix-form-container" [formGroup]="form" (submit)="onSubmit()">
<ix-input
type="number"
formControlName="size"
[label]="'Root Disk Size (in GiB)' | translate"
[required]="true"
></ix-input>

<ix-form-actions>
<button
mat-button
type="button"
ixTest="cancel"
matDialogClose
>
{{ 'Cancel' | translate }}
</button>

<button
mat-button
color="primary"
type="submit"
ixTest="save"
[disabled]="form.invalid"
>
{{ 'Save' | translate }}
</button>
</ix-form-actions>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:host {
display: block;
width: 350px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { of } from 'rxjs';
import { GiB } from 'app/constants/bytes.constant';
import { fakeSuccessfulJob } from 'app/core/testing/utils/fake-job.utils';
import { mockApi, mockJob } from 'app/core/testing/utils/mock-api.utils';
import { VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { IxFormHarness } from 'app/modules/forms/ix-forms/testing/ix-form.harness';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { ApiService } from 'app/modules/websocket/api.service';
import {
IncreaseRootDiskSizeComponent,
} from 'app/pages/virtualization/components/all-instances/instance-details/instance-disks/increase-root-disk-size/increase-root-disk-size.component';

describe('IncreaseRootDiskSizeComponent', () => {
let spectator: Spectator<IncreaseRootDiskSizeComponent>;
let loader: HarnessLoader;

const createComponent = createComponentFactory({
component: IncreaseRootDiskSizeComponent,
providers: [
mockApi([
mockJob('virt.instance.update', fakeSuccessfulJob()),
]),
mockProvider(SnackbarService),
mockProvider(MatDialogRef),
mockProvider(DialogService, {
jobDialog: jest.fn(() => ({
afterClosed: () => of(true),
})),
}),
{
provide: MAT_DIALOG_DATA,
useValue: {
id: 'test',
root_disk_size: 2 * GiB,
} as VirtualizationInstance,
},
],
});

beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('shows current root disk size', async () => {
const form = await loader.getHarness(IxFormHarness);

expect(await form.getValues()).toEqual({
'Root Disk Size (in GiB)': '2',
});
});

it('increases root disk size when new value is set', async () => {
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
'Root Disk Size (in GiB)': '4',
});

const saveButton = await loader.getHarness(MatButtonHarness.with({ text: 'Save' }));
await saveButton.click();

expect(spectator.inject(ApiService).job).toHaveBeenCalledWith('virt.instance.update', [
'test',
{ root_disk_size: 4 },
]);
expect(spectator.inject(DialogService).jobDialog).toHaveBeenCalled();
expect(spectator.inject(SnackbarService).success).toHaveBeenCalled();
expect(spectator.inject(MatDialogRef).close).toHaveBeenCalledWith(4);
});

it('does not allow value that is smaller than previous root disk size', async () => {
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
'Root Disk Size (in GiB)': '1',
});

const input = await form.getControl('Root Disk Size (in GiB)');
expect(await input.getErrorText()).toBe('Minimum value is 2');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
ChangeDetectionStrategy, Component, Inject,
} from '@angular/core';
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import {
MAT_DIALOG_DATA, MatDialogClose, MatDialogRef, MatDialogTitle,
} from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { GiB } from 'app/constants/bytes.constant';
import { VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { FormActionsComponent } from 'app/modules/forms/ix-forms/components/form-actions/form-actions.component';
import { IxInputComponent } from 'app/modules/forms/ix-forms/components/ix-input/ix-input.component';
import { IxFormatterService } from 'app/modules/forms/ix-forms/services/ix-formatter.service';
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 { ErrorHandlerService } from 'app/services/error-handler.service';

@UntilDestroy()
@Component({
selector: 'ix-increase-root-disk-size',
templateUrl: './increase-root-disk-size.component.html',
styleUrls: ['./increase-root-disk-size.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
IxInputComponent,
MatButton,
MatDialogClose,
MatDialogTitle,
ReactiveFormsModule,
TestDirective,
TranslateModule,
FormActionsComponent,
],
})
export class IncreaseRootDiskSizeComponent {
protected readonly form = this.formBuilder.group({
size: [0],
});

constructor(
@Inject(MAT_DIALOG_DATA) private instance: VirtualizationInstance,
private formBuilder: NonNullableFormBuilder,
private errorHandler: ErrorHandlerService,
private dialogService: DialogService,
private api: ApiService,
private translate: TranslateService,
private snackbar: SnackbarService,
private dialogRef: MatDialogRef<IncreaseRootDiskSizeComponent>,
protected formatter: IxFormatterService,
) {
this.form.setValue({
size: this.instance.root_disk_size / GiB,
});

this.form.controls.size.addValidators(Validators.min(this.instance.root_disk_size / GiB));
}

onSubmit(): void {
this.dialogService.jobDialog(
this.api.job('virt.instance.update', [this.instance.id, { root_disk_size: this.form.value.size }]),
{ title: this.translate.instant('Increasing disk size') },
)
.afterClosed()
.pipe(
this.errorHandler.catchError(),
untilDestroyed(this),
)
.subscribe(() => {
this.dialogRef.close(this.form.value.size);
this.snackbar.success(this.translate.instant('Disk size increased'));
});
}
}
4 changes: 4 additions & 0 deletions src/assets/i18n/af.json
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,7 @@
"Disk not attached to any pools.": "",
"Disk saved": "",
"Disk settings successfully saved.": "",
"Disk size increased": "",
"Disks": "",
"Disks Overview": "",
"Disks on {enclosure}": "",
Expand Down Expand Up @@ -2269,7 +2270,10 @@
"Incorrect Password": "",
"Incorrect crontab value.": "",
"Incorrect or expired OTP. Please try again.": "",
"Increase": "",
"Increase Root Disk Size": "",
"Increase logging verbosity related to the active directory service in /var/log/middlewared.log": "",
"Increasing disk size": "",
"InfluxDB time series name for collected points.": "",
"Info": "",
"Informational": "",
Expand Down
4 changes: 4 additions & 0 deletions src/assets/i18n/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,7 @@
"Disk not attached to any pools.": "",
"Disk saved": "",
"Disk settings successfully saved.": "",
"Disk size increased": "",
"Disks": "",
"Disks Overview": "",
"Disks on {enclosure}": "",
Expand Down Expand Up @@ -2269,7 +2270,10 @@
"Incorrect Password": "",
"Incorrect crontab value.": "",
"Incorrect or expired OTP. Please try again.": "",
"Increase": "",
"Increase Root Disk Size": "",
"Increase logging verbosity related to the active directory service in /var/log/middlewared.log": "",
"Increasing disk size": "",
"InfluxDB time series name for collected points.": "",
"Info": "",
"Informational": "",
Expand Down
Loading

0 comments on commit b93bfe9

Please sign in to comment.