Skip to content

Commit

Permalink
Merge branch 'master' of github.com:truenas/webui into NAS-132720
Browse files Browse the repository at this point in the history
  • Loading branch information
undsoft committed Nov 25, 2024
2 parents 5ccc674 + 7ad0021 commit 4ea5bcd
Show file tree
Hide file tree
Showing 199 changed files with 2,095 additions and 627 deletions.
Original file line number Diff line number Diff line change
@@ -1,54 +1,22 @@
import { Router } from '@angular/router';
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest';
import { MockProvider } from 'ng-mocks';
import { createDirectiveFactory, mockProvider, SpectatorDirective } from '@ngneat/spectator/jest';
import { NavigateAndInteractService } from 'app/directives/navigate-and-interact/navigate-and-interact.service';
import { NavigateAndInteractDirective } from './navigate-and-interact.directive';

describe('NavigateAndInteractDirective', () => {
let spectator: SpectatorDirective<NavigateAndInteractDirective>;
let mockRouter: Router;
const createDirective = createDirectiveFactory({
directive: NavigateAndInteractDirective,
providers: [
MockProvider(Router, {
navigate: jest.fn(() => Promise.resolve(true)),
}),
mockProvider(NavigateAndInteractService),
],
});

beforeEach(() => {
spectator = createDirective('<div ixNavigateAndInteract [navigateRoute]="[\'/some-path\']" navigateHash="testHash"></div>');
mockRouter = spectator.inject(Router);
});

it('should create an instance', () => {
expect(spectator.directive).toBeTruthy();
});

it('should call router.navigate with correct parameters on click', () => {
spectator.dispatchMouseEvent(spectator.element, 'click');
expect(mockRouter.navigate).toHaveBeenCalledWith(['/some-path'], { fragment: 'testHash' });
});

it('should scroll to and highlight the element with the given ID', () => {
const scrollIntoViewMock = jest.fn();
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;

const mockElement = document.createElement('div');
mockElement.id = 'testHash';
document.body.appendChild(mockElement);

const clickSpy = jest.spyOn(HTMLElement.prototype, 'click');

it('calls NavigateAndInteractService.navigateAndInteract when element is clicked', () => {
spectator.dispatchMouseEvent(spectator.element, 'click');

setTimeout(() => {
expect(scrollIntoViewMock).toHaveBeenCalled();
expect(clickSpy).toHaveBeenCalled();

// Clean up
document.body.removeChild(mockElement);
// Restore original scrollIntoView
delete HTMLElement.prototype.scrollIntoView;
}, 0);
expect(spectator.inject(NavigateAndInteractService).navigateAndInteract).toHaveBeenCalledWith(['/some-path'], 'testHash');
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
Directive, HostListener, input,
} from '@angular/core';
import { Router } from '@angular/router';
import { UntilDestroy } from '@ngneat/until-destroy';
import { NavigateAndInteractService } from 'app/directives/navigate-and-interact/navigate-and-interact.service';

@UntilDestroy()
@Directive({
Expand All @@ -13,25 +13,10 @@ export class NavigateAndInteractDirective {
readonly navigateRoute = input.required<string[]>();
readonly navigateHash = input.required<string>();

constructor(private router: Router) {}
constructor(private navigateAndInteract: NavigateAndInteractService) {}

@HostListener('click')
onClick(): void {
this.router.navigate(this.navigateRoute(), { fragment: this.navigateHash() }).then(() => {
const htmlElement = document.getElementById(this.navigateHash());
if (htmlElement) {
this.handleHashScrollIntoView(htmlElement);
}
});
}

private handleHashScrollIntoView(htmlElement: HTMLElement): void {
const highlightedClass = 'highlighted-element';
setTimeout(() => {
htmlElement.scrollIntoView({ block: 'center' });
htmlElement.classList.add(highlightedClass);
htmlElement.click();
}, 150);
setTimeout(() => htmlElement.classList.remove(highlightedClass), 2150);
this.navigateAndInteract.navigateAndInteract(this.navigateRoute(), this.navigateHash());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Router } from '@angular/router';
import {
createServiceFactory,
SpectatorService,
} from '@ngneat/spectator/jest';
import { MockProvider } from 'ng-mocks';
import { NavigateAndInteractService } from 'app/directives/navigate-and-interact/navigate-and-interact.service';

describe('NavigateAndInteractService', () => {
let spectator: SpectatorService<NavigateAndInteractService>;
const createComponent = createServiceFactory({
service: NavigateAndInteractService,
providers: [
MockProvider(Router, {
navigate: jest.fn(() => Promise.resolve(true)),
}),
],
});

beforeEach(() => {
spectator = createComponent();
});

it('should call router.navigate with correct parameters on click', () => {
spectator.service.navigateAndInteract(['/some-path'], 'testHash');
expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(['/some-path'], { fragment: 'testHash' });
});

it('should scroll to and highlight the element with the given ID', () => {
const scrollIntoViewMock = jest.fn();
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;

const mockElement = document.createElement('div');
mockElement.id = 'testHash';
document.body.appendChild(mockElement);

const clickSpy = jest.spyOn(HTMLElement.prototype, 'click');

spectator.service.navigateAndInteract(['/some-path'], 'testHash');

setTimeout(() => {
expect(scrollIntoViewMock).toHaveBeenCalled();
expect(clickSpy).toHaveBeenCalled();

// Clean up
document.body.removeChild(mockElement);
// Restore original scrollIntoView
delete HTMLElement.prototype.scrollIntoView;
}, 0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { WINDOW } from 'app/helpers/window.helper';

@Injectable({
providedIn: 'root',
})
export class NavigateAndInteractService {
constructor(
private router: Router,
@Inject(WINDOW) private window: Window,
) {}

navigateAndInteract(route: string[], hash: string): void {
this.router.navigate(route, { fragment: hash }).then(() => {
setTimeout(() => {
const htmlElement = this.window.document.getElementById(hash);
if (htmlElement) {
this.handleHashScrollIntoView(htmlElement);
}
}, 150);
});
}

private handleHashScrollIntoView(htmlElement: HTMLElement): void {
const highlightedClass = 'highlighted-element';
htmlElement.scrollIntoView({ block: 'center' });
htmlElement.classList.add(highlightedClass);
htmlElement.click();
setTimeout(() => htmlElement.classList.remove(highlightedClass), 2150);
}
}
1 change: 1 addition & 0 deletions src/app/interfaces/api-error.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface ApiError {
reason: string;
trace: ApiErrorTrace;
type: ResponseErrorType | null;
message?: string | null;
}

export interface ApiErrorTrace {
Expand Down
15 changes: 15 additions & 0 deletions src/app/interfaces/api/api-call-directory.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ import {
FailoverConfig,
FailoverUpdate,
} from 'app/interfaces/failover.interface';
import {
FibreChannelPort,
FibreChannelPortChoices,
FibreChannelPortUpdate,
} from 'app/interfaces/fibre-channel.interface';
import { FileRecord, ListdirQueryParams } from 'app/interfaces/file-record.interface';
import { FileSystemStat, Statfs } from 'app/interfaces/filesystem-stat.interface';
import { FtpConfig, FtpConfigUpdate } from 'app/interfaces/ftp-config.interface';
Expand Down Expand Up @@ -449,6 +454,16 @@ export interface ApiCallDirectory {
'failover.sync_to_peer': { params: [{ reboot?: boolean }]; response: void };
'failover.update': { params: [FailoverUpdate]; response: FailoverConfig };

// Fibre Channel
'fc.capable': { params: []; response: boolean };

// Fibre Channel Port
'fcport.create': { params: [FibreChannelPortUpdate]; response: FibreChannelPort };
'fcport.update': { params: [id: number, update: FibreChannelPortUpdate]; response: FibreChannelPort };
'fcport.delete': { params: [id: number]; response: true };
'fcport.port_choices': { params: [include_used?: boolean]; response: FibreChannelPortChoices };
'fcport.query': { params: QueryParams<FibreChannelPort>; response: FibreChannelPort[] };

// Filesystem
'filesystem.acltemplate.by_path': { params: [AclTemplateByPathParams]; response: AclTemplateByPath[] };
'filesystem.acltemplate.create': { params: [AclTemplateCreateParams]; response: AclTemplateCreateResponse };
Expand Down
17 changes: 17 additions & 0 deletions src/app/interfaces/fibre-channel.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface FibreChannelPort {
id: number;
port: string;
wwpn: string | null;
wwpn_b: string | null;
target: unknown; // TODO: Probably IscsiTarget
}

export interface FibreChannelPortUpdate {
port: string;
target_id: number;
}

export type FibreChannelPortChoices = Record<string, {
wwpn: string;
wwpn_b: string;
}>;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
}

<mat-button-toggle-group
ixRegisteredControl
[value]="value"
[disabled]="isDisabled"
[ixTest]="controlDirective.name"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
}

<div
ixRegisteredControl
class="checkbox-list"
[class.inline]="inlineFields"
[attr.aria-label]="label"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<mat-checkbox
ixRegisteredControl
color="primary"
[checked]="value"
[required]="required"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
AfterViewInit,
ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnDestroy,
ChangeDetectionStrategy, ChangeDetectorRef, Component, Input,
} from '@angular/core';
import {
ControlValueAccessor, NgControl,
Expand All @@ -10,7 +9,6 @@ import { MatHint } from '@angular/material/form-field';
import { UntilDestroy } from '@ngneat/until-destroy';
import { IxErrorsComponent } from 'app/modules/forms/ix-forms/components/ix-errors/ix-errors.component';
import { WarningComponent } from 'app/modules/forms/ix-forms/components/warning/warning.component';
import { IxFormService } from 'app/modules/forms/ix-forms/services/ix-form.service';
import { TestDirective } from 'app/modules/test-id/test.directive';
import { TooltipComponent } from 'app/modules/tooltip/tooltip.component';

Expand All @@ -30,7 +28,7 @@ import { TooltipComponent } from 'app/modules/tooltip/tooltip.component';
TestDirective,
],
})
export class IxCheckboxComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
export class IxCheckboxComponent implements ControlValueAccessor {
@Input() label: string;
@Input() hint: string;
@Input() tooltip: string;
Expand All @@ -43,20 +41,10 @@ export class IxCheckboxComponent implements ControlValueAccessor, AfterViewInit,
constructor(
public controlDirective: NgControl,
private cdr: ChangeDetectorRef,
private formService: IxFormService,
private elementRef: ElementRef<HTMLElement>,
) {
this.controlDirective.valueAccessor = this;
}

ngAfterViewInit(): void {
this.formService.registerControl(this.controlDirective, this.elementRef);
}

ngOnDestroy(): void {
this.formService.unregisterControl(this.controlDirective);
}

onChange: (value: boolean) => void = (): void => {};
onTouch: () => void = (): void => {};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<mat-chip-grid
#chipList
class="form-chip"
ixRegisteredControl
[disabled]="isDisabled"
[required]="required"
[attr.aria-label]="label"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
></ix-label>
}

<div class="input-container" [class.disabled]="disabledState$ | async">
<div
ixRegisteredControl
class="input-container"
[class.disabled]="disabledState$ | async"
>
<div #inputArea></div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<input
#ixInput
matInput
ixRegisteredControl
[value]="selectedOption?.label || textContent"
[placeholder]="allowCustomValue ? ('Search or enter value' | translate) : ('Search' | translate)"
[disabled]="isDisabled"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
matInput
type="text"
autocomplete="off"
ixRegisteredControl
[value]="inputValue"
[ixTest]="controlDirective.name"
[required]="required"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { IxErrorsComponent } from 'app/modules/forms/ix-forms/components/ix-erro
import { CreateDatasetDialogComponent } from 'app/modules/forms/ix-forms/components/ix-explorer/create-dataset-dialog/create-dataset-dialog.component';
import { TreeNodeProvider } from 'app/modules/forms/ix-forms/components/ix-explorer/tree-node-provider.interface';
import { IxLabelComponent } from 'app/modules/forms/ix-forms/components/ix-label/ix-label.component';
import { RegisteredControlDirective } from 'app/modules/forms/ix-forms/directives/registered-control.directive';
import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component';
import { TestOverrideDirective } from 'app/modules/test-id/test-override/test-override.directive';
import { TestDirective } from 'app/modules/test-id/test.directive';
Expand All @@ -55,6 +56,7 @@ import { TestDirective } from 'app/modules/test-id/test.directive';
RequiresRolesDirective,
TestDirective,
TestOverrideDirective,
RegisteredControlDirective,
],
})
export class IxExplorerComponent implements OnInit, OnChanges, ControlValueAccessor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<input
#fileInput
type="file"
ixRegisteredControl
[accept]="acceptedFiles"
[ixTest]="controlDirective.name"
[required]="required"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { UntilDestroy } from '@ngneat/until-destroy';
import { TranslateModule } from '@ngx-translate/core';
import { IxErrorsComponent } from 'app/modules/forms/ix-forms/components/ix-errors/ix-errors.component';
import { IxLabelComponent } from 'app/modules/forms/ix-forms/components/ix-label/ix-label.component';
import { RegisteredControlDirective } from 'app/modules/forms/ix-forms/directives/registered-control.directive';
import { IxFormatterService } from 'app/modules/forms/ix-forms/services/ix-formatter.service';
import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component';
import { TestOverrideDirective } from 'app/modules/test-id/test-override/test-override.directive';
Expand All @@ -29,6 +30,7 @@ import { TestDirective } from 'app/modules/test-id/test.directive';
TranslateModule,
TestDirective,
TestOverrideDirective,
RegisteredControlDirective,
],
})
export class IxFileInputComponent implements ControlValueAccessor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
mat-icon-button
role="radio"
type="button"
ixRegisteredControl
[disabled]="isDisabled"
[ixTest]="[controlDirective.name, option.label]"
[attr.aria-label]="option.label | translate"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#ixInput
#trigger="matAutocompleteTrigger"
matInput
ixRegisteredControl
[class.prefix-padding]="prefixIcon"
[class.password-field]="isPasswordField()"
[class.has-reset-input-icon]="shouldShowResetInput()"
Expand Down
Loading

0 comments on commit 4ea5bcd

Please sign in to comment.