Skip to content

Commit

Permalink
Merge branch 'master' into NAS-132938
Browse files Browse the repository at this point in the history
  • Loading branch information
denysbutenko committed Dec 27, 2024
2 parents 7301367 + 57f6284 commit b635051
Show file tree
Hide file tree
Showing 543 changed files with 5,111 additions and 2,873 deletions.
3 changes: 3 additions & 0 deletions eslint/eslint-spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export const eslintSpec = {
jest.configs['flat/style']
],
rules: {
// Allow ! assertion in tests.
'@typescript-eslint/no-non-null-assertion': 'off',

"jest/no-large-snapshots": ["error"],
"jest/prefer-equality-matcher": ["error"],
"jest/prefer-lowercase-title": ["error", {"ignore": ["describe"]}],
Expand Down
2 changes: 2 additions & 0 deletions eslint/eslint-ts-rules-fix-later.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const fixLaterRules = {
"@typescript-eslint/prefer-regexp-exec": ["off"],
"@typescript-eslint/no-dynamic-delete": ["off"],
"@typescript-eslint/class-literal-property-style": ["off"],
// TODO: Reenable after strict null checks have been implemented
"@typescript-eslint/no-unnecessary-type-assertion": ["off"],
"no-prototype-builtins": ["off"],
"@smarttools/rxjs/no-nested-subscribe": ["off"],

Expand Down
2 changes: 1 addition & 1 deletion src/app/directives/autofocus/autofocus.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ export class AutofocusDirective implements AfterViewInit {
// Do not intercept focus when a full-screen dialog is open.
const rootNode = this.host.nativeElement.getRootNode() as HTMLElement;
if (rootNode.querySelector('ix-full-screen-dialog')) return;
this.host.nativeElement.querySelector<HTMLElement>('input, textarea, select').focus();
this.host.nativeElement.querySelector<HTMLElement>('input, textarea, select')?.focus();
}
}
2 changes: 1 addition & 1 deletion src/app/directives/has-access/has-access.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { MissingAccessWrapperComponent } from 'app/directives/has-access/missing
})
export class HasAccessDirective {
private wrapperContainer: ComponentRef<MissingAccessWrapperComponent>;
private previousAccess: boolean = null;
private previousAccess: boolean | null = null;

// eslint-disable-next-line @angular-eslint/prefer-signals
@Input()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component';
],
})
export class MissingAccessWrapperComponent {
readonly template = input<TemplateRef<HTMLElement>>();
readonly template = input.required<TemplateRef<HTMLElement>>();
readonly class = input<string>();
}
4 changes: 2 additions & 2 deletions src/app/directives/ui-search.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class UiSearchDirective implements OnInit, OnDestroy {
return hierarchyItem;
}

private highlightTimeout: Timeout = null;
private highlightTimeout: Timeout | null = null;

constructor(
private renderer: Renderer2,
Expand Down Expand Up @@ -96,7 +96,7 @@ export class UiSearchDirective implements OnInit, OnDestroy {
setTimeout(() => {
anchorRef.focus();
anchorRef.scrollIntoView();
document.querySelector<HTMLElement>('.rightside-content-hold').scrollBy(0, -20);
document.querySelector<HTMLElement>('.rightside-content-hold')?.scrollBy(0, -20);
['click', 'keydown'].forEach((event) => document.addEventListener(event, removeHighlightStyling, { once: true }));
}, searchDelayConst);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { OldSlideInRef } from 'app/modules/slide-ins/old-slide-in-ref';
standalone: true,
})
export class WarnAboutUnsavedChangesDirective<T> implements OnInit {
readonly formGroup = input<FormGroup>();
readonly formGroup = input.required<FormGroup>();

private formSubmitted = false;

Expand Down
2 changes: 1 addition & 1 deletion src/app/enums/docker-config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface DockerAddressPool {
}

export interface DockerConfigUpdate {
pool?: string;
pool?: string | null;
nvidia?: boolean;
address_pools?: DockerAddressPool[];
enable_image_updates?: boolean;
Expand Down
6 changes: 4 additions & 2 deletions src/app/helpers/operators/select-not-null.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { Selector } from '@ngrx/store/src/models';
import { Observable, pipe, UnaryFunction } from 'rxjs';
import { filter } from 'rxjs/operators';

export function selectNotNull<S, R>(selector: Selector<S, R>): UnaryFunction<Observable<S>, Observable<R>> {
export function selectNotNull<S, R>(
selector: Selector<S, R>,
): UnaryFunction<Observable<S>, Observable<Exclude<R, null>>> {
return pipe(
select(selector),
filter((value) => value !== null),
filter((value): value is Exclude<R, null> => value !== null),
);
}
4 changes: 2 additions & 2 deletions src/app/interfaces/api/api-call-directory.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ export interface ApiCallDirectory {
'fc.capable': { params: []; response: boolean };

// Fibre Channel Host
'fc.fc_host.query': { params: []; response: FibreChannelHost[] };
'fc.fc_host.query': { params: QueryParams<FibreChannelHost>; response: FibreChannelHost[] };
'fc.fc_host.update': { params: [id: number, changes: Partial<FibreChannelHost>]; response: void };

// Fibre Channel Port
Expand Down Expand Up @@ -560,7 +560,7 @@ export interface ApiCallDirectory {
'iscsi.portal.query': { params: QueryParams<IscsiPortal>; response: IscsiPortal[] };
'iscsi.portal.update': { params: [id: number, target: IscsiPortalUpdate]; response: IscsiPortal };
'iscsi.target.create': { params: [IscsiTargetUpdate]; response: IscsiTarget };
'iscsi.target.delete': { params: [id: number, force?: boolean]; response: boolean };
'iscsi.target.delete': { params: [id: number, force?: boolean, delete_extents?: boolean]; response: boolean };
'iscsi.target.query': { params: QueryParams<IscsiTarget>; response: IscsiTarget[] };
'iscsi.target.update': { params: [id: number, target: IscsiTargetUpdate]; response: IscsiTarget };
'iscsi.targetextent.create': { params: [IscsiTargetExtentUpdate]; response: IscsiTargetExtent };
Expand Down
3 changes: 3 additions & 0 deletions src/app/interfaces/bind-ip.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface BindIp {
$ipv4_interface: string;
}
2 changes: 0 additions & 2 deletions src/app/interfaces/cloud-sync-task.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ export interface CloudSyncTaskUpdate extends Omit<CloudSyncTask, 'id' | 'job' |

export interface CloudSyncTaskUi extends CloudSyncTask {
credential: string;
cron_schedule: string;
frequency: string;
next_run: string;
next_run_time: Date | string;
state: DataProtectionTaskState;
Expand Down
14 changes: 7 additions & 7 deletions src/app/interfaces/enclosure.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export type EnclosureElements = {
export interface EnclosureElement {
descriptor: string;
status: string;
value?: string;
value: string | null;
value_raw?: number | string;
}

Expand All @@ -79,18 +79,18 @@ export interface DashboardEnclosureSlot {
drive_bay_number?: number;
descriptor: string;
status: EnclosureStatus;
dev: string;
dev: string | null;
supports_identify_light?: boolean;
drive_bay_light_status: DriveBayLightStatus | null;
size?: number;
model?: string;
size?: number | null;
model?: string | null;
is_top: boolean;
is_front: boolean;
is_rear: boolean;
is_internal: boolean;
serial?: string;
type?: DiskType;
rotationrate?: number;
serial?: string | null;
type?: DiskType | null;
rotationrate?: number | null;
pool_info: EnclosureSlotPoolInfo | null;
}

Expand Down
2 changes: 2 additions & 0 deletions src/app/interfaces/option.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ export interface ActionOption<T = BaseOptionValueType> extends Option<T> {
}

export const newOption = 'NEW';
export const nullOption = 'NULL';
export const skipOption = 'SKIP';
2 changes: 0 additions & 2 deletions src/app/interfaces/periodic-snapshot-task.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ export interface PeriodicSnapshotTaskUpdate extends PeriodicSnapshotTaskCreate {

export interface PeriodicSnapshotTaskUi extends PeriodicSnapshotTask {
keepfor: string;
cron_schedule: string;
when: string;
frequency: string;
next_run: string;
last_run: string;
legacy: boolean;
Expand Down
2 changes: 0 additions & 2 deletions src/app/interfaces/rsync-task.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ export type RsyncTaskUpdate = {
} & Omit<RsyncTask, 'id' | 'job' | 'locked' | 'ssh_credentials'>;

export interface RsyncTaskUi extends RsyncTask {
cron_schedule: string;
next_run: string;
frequency: string;
state: DataProtectionTaskState;
last_run: string;
}
2 changes: 0 additions & 2 deletions src/app/interfaces/smart-test.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ export interface SmartTestTask {
export type SmartTestTaskUpdate = Omit<SmartTestTask, 'id'>;

export interface SmartTestTaskUi extends SmartTestTask {
cron_schedule: string;
frequency: string;
next_run: string;
disksLabel?: string[];
}
Expand Down
3 changes: 2 additions & 1 deletion src/app/interfaces/smb-config.interface.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { SmbEncryption } from 'app/enums/smb-encryption.enum';
import { BindIp } from 'app/interfaces/bind-ip.interface';

export interface SmbConfig {
aapl_extensions: boolean;
admin_group: string;
bindip: string[];
bindip: BindIp[];
cifs_SID: string;
description: string;
dirmask: string;
Expand Down
18 changes: 17 additions & 1 deletion src/app/modules/alerts/components/alert/alert.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,23 @@ <h3 [class]="['alert-level', alertLevelColor]">
{{ levelLabel() }}
</h3>
}
<h4 class="alert-message" [innerHTML]="alert().formatted"></h4>
<h4
#alertMessage
class="alert-message"
[class.collapsed]="isCollapsed()"
[innerHTML]="alert().formatted"
></h4>
@if (isExpandable()) {
<button
mat-button
class="expand-collapse-button"
[ixTest]="[alert().key, 'expand']"
[class.collapsed]="isCollapsed()"
(click)="toggleCollapse()"
>
{{ isCollapsed() ? ('Expand' | translate) : ('Collapse' | translate) }}
</button>
}
@if (isHaLicensed()) {
<div class="alert-node">{{ alert().node }}</div>
}
Expand Down
25 changes: 25 additions & 0 deletions src/app/modules/alerts/components/alert/alert.component.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import 'mixins/text';

:host {
align-items: center;
display: flex;
Expand Down Expand Up @@ -48,6 +50,29 @@
line-height: inherit;
margin-bottom: 6px;
margin-top: 3px;

&.collapsed {
@include line-clamp(5);
}
}

.expand-collapse-button {
background-color: var(--alt-bg1);
cursor: pointer;
display: flex;
font-size: inherit;
font-weight: normal;
height: inherit;
justify-content: center;
line-height: inherit;
margin: 0 0 5px;
opacity: 0.9;
padding: 0;
width: max-content;

&.collapsed {
margin-top: -8px;
}
}

.alert-node {
Expand Down
26 changes: 26 additions & 0 deletions src/app/modules/alerts/components/alert/alert.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatButtonHarness } from '@angular/material/button/testing';
import { By } from '@angular/platform-browser';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { EffectsModule } from '@ngrx/effects';
import { Store, StoreModule } from '@ngrx/store';
Expand Down Expand Up @@ -34,6 +38,8 @@ describe('AlertComponent', () => {
let spectator: Spectator<AlertComponent>;
let api: ApiService;
let alert: AlertPageObject;
let loader: HarnessLoader;

const createComponent = createComponentFactory({
component: AlertComponent,
imports: [
Expand Down Expand Up @@ -73,6 +79,7 @@ describe('AlertComponent', () => {

api = spectator.inject(ApiService);
alert = new AlertPageObject(spectator);
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('shows alert level', () => {
Expand Down Expand Up @@ -127,4 +134,23 @@ describe('AlertComponent', () => {
const state = await firstValueFrom(spectator.inject(Store).pipe(map(selectAlerts)));
expect(state).toEqual([dummyAlert]);
});

it('shows expand/collapse button when alert message is too long', async () => {
const longMessage = 'This is a very long alert message '.repeat(10);
spectator.setInput('alert', { ...dummyAlert, formatted: longMessage } as Alert);

const alertMessageElement = spectator.debugElement.query(By.css('.alert-message')).nativeElement as HTMLElement;
jest.spyOn(alertMessageElement, 'scrollHeight', 'get').mockReturnValue(300);
jest.spyOn(alertMessageElement, 'offsetHeight', 'get').mockReturnValue(100);

spectator.component.ngAfterViewInit();

const expandButton = await loader.getHarness(MatButtonHarness.with({ text: 'Expand' }));
expect(expandButton).toExist();

await expandButton.click();

const collapseButton = await loader.getHarness(MatButtonHarness.with({ text: 'Collapse' }));
expect(collapseButton).toExist();
});
});
33 changes: 28 additions & 5 deletions src/app/modules/alerts/components/alert/alert.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { AsyncPipe } from '@angular/common';
import {
ChangeDetectionStrategy, Component, computed, HostBinding, input, OnChanges,
AfterViewInit,
ChangeDetectionStrategy, Component, computed, ElementRef, HostBinding, input, OnChanges,
signal,
ViewChild,
} from '@angular/core';
import { MatButton } from '@angular/material/button';
import { MatTooltip } from '@angular/material/tooltip';
import { UntilDestroy } from '@ngneat/until-destroy';
import { Store } from '@ngrx/store';
Expand Down Expand Up @@ -43,19 +47,26 @@ enum AlertLevelColor {
imports: [
IxIconComponent,
MatTooltip,
MatButton,
TestDirective,
TranslateModule,
FormatDateTimePipe,
AsyncPipe,
RequiresRolesDirective,
],
})
export class AlertComponent implements OnChanges {
export class AlertComponent implements OnChanges, AfterViewInit {
readonly alert = input.required<Alert>();
readonly isHaLicensed = input<boolean>();
readonly requiredRoles = [Role.AlertListWrite];

alertLevelColor: AlertLevelColor;
@ViewChild('alertMessage', { static: true }) alertMessage: ElementRef<HTMLElement>;

protected isCollapsed = signal<boolean>(true);
protected isExpandable = signal<boolean>(false);

protected readonly requiredRoles = [Role.AlertListWrite];

alertLevelColor: AlertLevelColor | undefined;
icon: string;
iconTooltip: string;

Expand All @@ -71,12 +82,24 @@ export class AlertComponent implements OnChanges {
private translate: TranslateService,
) {}

readonly levelLabel = computed(() => this.translate.instant(alertLevelLabels.get(this.alert().level)));
readonly levelLabel = computed(() => {
const levelLabel = alertLevelLabels.get(this.alert().level) || this.alert().level;
return this.translate.instant(levelLabel);
});

ngOnChanges(): void {
this.setStyles();
}

ngAfterViewInit(): void {
const alertMessageElement = this.alertMessage.nativeElement;
this.isExpandable.set(alertMessageElement.scrollHeight > alertMessageElement.offsetHeight);
}

toggleCollapse(): void {
this.isCollapsed.set(!this.isCollapsed());
}

onDismiss(): void {
this.store$.dispatch(dismissAlertPressed({ id: this.alert().id }));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ describe('OauthButtonComponent', () => {
});

beforeEach(() => {
spectator = createComponent();
spectator = createComponent({
props: {
testId: 'oauth-button',
},
});
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

Expand Down
Loading

0 comments on commit b635051

Please sign in to comment.