diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6e4dc9a73f8..89880d0cf5e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -79,7 +79,7 @@ module.exports = { "order": "asc", "caseInsensitive": false } - }], + }], "import/no-duplicates": ["error", {"considerQueryString": true}], "import/extensions": ["error", "ignorePackages", { "js": "never", @@ -151,6 +151,16 @@ module.exports = { "sonarjs/no-empty-test-file": ["off"], "sonarjs/new-cap": ["off"], "sonarjs/no-ignored-exceptions": ["off"], + "sonarjs/array-callback-without-return": "off", + "sonarjs/os-command": "off", + "sonarjs/sonar-prefer-optional-chain": "off", + "@typescript-eslint/member-ordering": "off", + "@typescript-eslint/no-unsafe-return": "off", + "sonarjs/no-misused-promises": "off", + "sonarjs/function-return-type": "off", + "@typescript-eslint/no-unnecessary-type-parameters": "off", + "no-multi-str": "off", + "@typescript-eslint/prefer-reduce-type-parameter": ["off"], // TODO: Airbnb rules that are disabled for now as they cannot be fixed automatically "no-restricted-syntax": ["error", @@ -189,27 +199,7 @@ module.exports = { }, ], "no-param-reassign": "off", - "@typescript-eslint/no-loop-func": "off", "no-await-in-loop": "off", - "no-multi-str": "off", - "no-mixed-operators": ["error", { - groups: [ - // TODO: Some operators from default config not implemented. - ["&", "|", "^", "~", "<<", ">>", ">>>"], - ["==", "!=", "===", "!==", ">", ">=", "<", "<="], - ["&&", "||"], - ["in", "instanceof"] - ], - allowSamePrecedence: true - }], - "default-case": "off", - "@typescript-eslint/member-ordering": "off", - "@typescript-eslint/no-unsafe-return": "off", - "sonarjs/no-misused-promises": "off", - "sonarjs/function-return-type": "off", - "@typescript-eslint/no-unnecessary-type-parameters": "off", - "sonarjs/os-command": "off", - "sonarjs/sonar-prefer-optional-chain": "off", // Other temporary disables "sonarjs/prefer-nullish-coalescing": ["off"], @@ -222,7 +212,6 @@ module.exports = { "@typescript-eslint/no-unnecessary-condition": ["off"], "@typescript-eslint/no-invalid-void-type": ["off"], "@typescript-eslint/no-dynamic-delete": ["off"], - "@typescript-eslint/prefer-reduce-type-parameter": ["off"], "sonarjs/no-selector-parameter": ["off"], "sonarjs/concise-regex": ["off"], "sonarjs/regex-complexity": ["off"], @@ -236,11 +225,7 @@ module.exports = { "sonarjs/slow-regex": ["off"], "sonarjs/no-base-to-string": ["off"], "sonarjs/link-with-target-blank": ["off"], - "sonarjs/array-callback-without-return": "off", - "sonarjs/no-nested-assignment": "off", "sonarjs/reduce-initial-value": "off", - "sonarjs/use-type-alias": "off", - "sonarjs/no-dead-store": "off", // Other overwrites ...(stylistic.configs.customize({ @@ -307,7 +292,7 @@ module.exports = { } }, ], - "unicorn/filename-case": ["error", { case: "kebabCase"}], + "unicorn/filename-case": ["error", { case: "kebabCase" }], "unicorn/prefer-array-find": ["error"], "@angular-eslint/component-selector": ["error", { "type": "element", @@ -356,16 +341,18 @@ module.exports = { } ], "patterns": [{ - "group": [ "../**"], + "group": ["../**"], "message": "Use alias 'app' to replace part '../' of the path." }], }], "@shopify/typescript-prefer-singular-enums": "error", "@shopify/typescript-prefer-pascal-case-enums": "error", - "@shopify/prefer-early-return": ["error", { maximumStatements: 3 }], + "@shopify/prefer-early-return": ["error", {maximumStatements: 3}], "import/no-default-export": "error", "@typescript-eslint/consistent-indexed-object-style": "error", "@angular-eslint/prefer-on-push-component-change-detection": "error", + "default-case": "off", + "@typescript-eslint/switch-exhaustiveness-check": "error", // RxJS rules "rxjs/no-unsafe-takeuntil": ["error", { @@ -430,6 +417,7 @@ module.exports = { "addElements": ["a", "mat-row", "mat-slider", "table"] }], "@angular-eslint/template/prefer-control-flow": ['error'], + "@angular-eslint/template/no-positive-tabindex": ["error"], // TODO: To be enabled later '@angular-eslint/template/no-negated-async': ['off'], diff --git a/src/app/core/testing/mock-enclosure/mock-enclosure-websocket.service.ts b/src/app/core/testing/mock-enclosure/mock-enclosure-websocket.service.ts index cb28ca76d4a..0eb3f0b4ab6 100644 --- a/src/app/core/testing/mock-enclosure/mock-enclosure-websocket.service.ts +++ b/src/app/core/testing/mock-enclosure/mock-enclosure-websocket.service.ts @@ -48,8 +48,9 @@ export class MockEnclosureWebsocketService extends WebSocketService { return this.mockStorage.webuiDashboardEnclosureResponse() ?? undefined; case 'truenas.is_ix_hardware': return true; + default: + return undefined; } - return undefined; } private postCallOverride(method: M, response: ApiCallResponse): ApiCallResponse { @@ -57,8 +58,8 @@ export class MockEnclosureWebsocketService extends WebSocketService { case 'webui.main.dashboard.sys_info': case 'system.info': return this.mockStorage.enhanceSystemInfoResponse(response as SystemInfo); + default: + return undefined; } - - return undefined; } } diff --git a/src/app/helpers/assert-unreachable.utils.ts b/src/app/helpers/assert-unreachable.utils.ts index 74b725989fb..fcb867d5b30 100644 --- a/src/app/helpers/assert-unreachable.utils.ts +++ b/src/app/helpers/assert-unreachable.utils.ts @@ -5,7 +5,8 @@ * * If you get a type error that something is not assignable to never, * then you forgot to handle a case in a switch statement. - * DO NOT JUST USE `as never` TO SILENCE THE ERROR. + * + * DO NOT just use `as never` to silence the error. */ export function assertUnreachable(value: never): void { console.error(`No such case in exhaustive switch: ${String(value)}`); diff --git a/src/app/modules/charts/gauge-chart/gauge-chart.component.ts b/src/app/modules/charts/gauge-chart/gauge-chart.component.ts index af2e02d648b..57eeed3853b 100644 --- a/src/app/modules/charts/gauge-chart/gauge-chart.component.ts +++ b/src/app/modules/charts/gauge-chart/gauge-chart.component.ts @@ -86,6 +86,7 @@ export class GaugeChartComponent { private conversionColor(color: string): string { const colorType = (new ThemeUtils()).getValueType(color); let resultColor = color; + // eslint-disable-next-line default-case switch (colorType) { case 'cssVar': { const cssVar = color.replace('var(--', '').replace(')', '') as keyof Theme; diff --git a/src/app/modules/dialog/components/job-progress/job-progress-dialog.component.ts b/src/app/modules/dialog/components/job-progress/job-progress-dialog.component.ts index 797546ea146..c1aa2337184 100644 --- a/src/app/modules/dialog/components/job-progress/job-progress-dialog.component.ts +++ b/src/app/modules/dialog/components/job-progress/job-progress-dialog.component.ts @@ -160,6 +160,7 @@ export class JobProgressDialogComponent implements OnInit, AfterViewChecked { this.dialogRef.close(); }, complete: () => { + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (this.job.state) { case JobState.Failed: this.jobFailure.emit(this.job); diff --git a/src/app/modules/empty/empty.component.ts b/src/app/modules/empty/empty.component.ts index bd821b343ae..091165d76c7 100644 --- a/src/app/modules/empty/empty.component.ts +++ b/src/app/modules/empty/empty.component.ts @@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { RequiresRolesDirective } from 'app/directives/requires-roles/requires-roles.directive'; import { EmptyType } from 'app/enums/empty-type.enum'; import { Role } from 'app/enums/role.enum'; +import { assertUnreachable } from 'app/helpers/assert-unreachable.utils'; import { EmptyConfig } from 'app/interfaces/empty-config.interface'; import { iconMarker, MarkedIcon } from 'app/modules/ix-icon/icon-marker.util'; import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component'; @@ -61,6 +62,8 @@ export class EmptyComponent { case EmptyType.NoSearchResults: icon = iconMarker('mdi-magnify-scan'); break; + default: + assertUnreachable(this.conf.type); } } return icon; diff --git a/src/app/modules/ix-table/components/ix-empty-row/ix-empty-row.component.ts b/src/app/modules/ix-table/components/ix-empty-row/ix-empty-row.component.ts index 464ad3218c2..caf981611fc 100644 --- a/src/app/modules/ix-table/components/ix-empty-row/ix-empty-row.component.ts +++ b/src/app/modules/ix-table/components/ix-empty-row/ix-empty-row.component.ts @@ -6,6 +6,7 @@ import { MatButton } from '@angular/material/button'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { TranslateService, TranslateModule } from '@ngx-translate/core'; import { EmptyType } from 'app/enums/empty-type.enum'; +import { assertUnreachable } from 'app/helpers/assert-unreachable.utils'; import { EmptyConfig } from 'app/interfaces/empty-config.interface'; import { iconMarker, MarkedIcon } from 'app/modules/ix-icon/icon-marker.util'; import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component'; @@ -79,6 +80,8 @@ export class IxTableEmptyRowComponent implements AfterViewInit { case EmptyType.NoSearchResults: icon = iconMarker('mdi-magnify-scan'); break; + default: + assertUnreachable(this.conf.type); } } return icon; diff --git a/src/app/pages/apps/components/installed-apps/installed-apps.component.ts b/src/app/pages/apps/components/installed-apps/installed-apps.component.ts index dbeb79b41d3..a038d9c3a80 100644 --- a/src/app/pages/apps/components/installed-apps/installed-apps.component.ts +++ b/src/app/pages/apps/components/installed-apps/installed-apps.component.ts @@ -281,7 +281,7 @@ export class InstalledAppsComponent implements OnInit, AfterViewInit { } } - showLoadStatus(type: EmptyType): void { + showLoadStatus(type: EmptyType.FirstUse | EmptyType.NoPageData | EmptyType.Errors | EmptyType.NoSearchResults): void { switch (type) { case EmptyType.FirstUse: case EmptyType.NoPageData: diff --git a/src/app/pages/dashboard/widgets/network/widget-interface/widget-interface.component.ts b/src/app/pages/dashboard/widgets/network/widget-interface/widget-interface.component.ts index 87b86c679b1..11cf4c37e4a 100644 --- a/src/app/pages/dashboard/widgets/network/widget-interface/widget-interface.component.ts +++ b/src/app/pages/dashboard/widgets/network/widget-interface/widget-interface.component.ts @@ -115,7 +115,7 @@ export class WidgetInterfaceComponent implements WidgetComponent !!response.length), map((response) => { const [update] = response; - return (update.data as number[][]).map((row) => (row = row.slice(1).map((value) => value * kb))); + return (update.data as number[][]).map((row) => row.slice(1).map((value) => value * kb)); }), )); diff --git a/src/app/pages/data-protection/replication/replication-wizard/replication-wizard.component.ts b/src/app/pages/data-protection/replication/replication-wizard/replication-wizard.component.ts index 90822194d3b..03ae3b0be42 100644 --- a/src/app/pages/data-protection/replication/replication-wizard/replication-wizard.component.ts +++ b/src/app/pages/data-protection/replication/replication-wizard/replication-wizard.component.ts @@ -422,7 +422,8 @@ export class ReplicationWizardComponent { if (requestsTasks.length) { return forkJoin(requestsTasks).pipe( map((createdSnapshotTasks) => { - return this.createdSnapshotTasks = (createdSnapshotTasks || []).filter((task) => !!task); + this.createdSnapshotTasks = (createdSnapshotTasks || []).filter((task) => !!task); + return this.createdSnapshotTasks; }), ); } diff --git a/src/app/pages/datasets/components/dataset-form/sections/other-options-section/other-options-section.component.ts b/src/app/pages/datasets/components/dataset-form/sections/other-options-section/other-options-section.component.ts index 86432d8245f..e5114290dea 100644 --- a/src/app/pages/datasets/components/dataset-form/sections/other-options-section/other-options-section.component.ts +++ b/src/app/pages/datasets/components/dataset-form/sections/other-options-section/other-options-section.component.ts @@ -299,6 +299,7 @@ export class OtherOptionsSectionComponent implements OnInit, OnChanges { return; } + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (aclTypeControl.value) { case DatasetAclType.Nfsv4: if (!this.existing) { diff --git a/src/app/pages/network/components/interface-form/interface-name-validator.service.ts b/src/app/pages/network/components/interface-form/interface-name-validator.service.ts index 0aaa5740179..7c76727bd1a 100644 --- a/src/app/pages/network/components/interface-form/interface-name-validator.service.ts +++ b/src/app/pages/network/components/interface-form/interface-name-validator.service.ts @@ -51,6 +51,8 @@ export class InterfaceNameValidatorService { case NetworkInterfaceType.Vlan: interfaceName = this.translate.instant('VLAN interface'); break; + default: + console.error('Unsupported interface type', type); } return this.translate.instant( diff --git a/src/app/pages/reports-dashboard/components/line-chart/line-chart.component.spec.ts b/src/app/pages/reports-dashboard/components/line-chart/line-chart.component.spec.ts index d860872b9bd..dbe73980340 100644 --- a/src/app/pages/reports-dashboard/components/line-chart/line-chart.component.spec.ts +++ b/src/app/pages/reports-dashboard/components/line-chart/line-chart.component.spec.ts @@ -51,7 +51,10 @@ describe('LineChartComponent', () => { describe('axisLabelFormatter', () => { it('returns default formatted value', () => { + jest.spyOn(console, 'warn').mockImplementation(); + expect(spectator.component.axisLabelFormatter(500000)).toBe('500k'); + expect(console.warn).toHaveBeenCalled(); }); it('returns formatted value when labelY is set', () => { diff --git a/src/app/pages/reports-dashboard/components/line-chart/line-chart.component.ts b/src/app/pages/reports-dashboard/components/line-chart/line-chart.component.ts index 8312930a0ed..a5c9dd7836c 100644 --- a/src/app/pages/reports-dashboard/components/line-chart/line-chart.component.ts +++ b/src/app/pages/reports-dashboard/components/line-chart/line-chart.component.ts @@ -173,10 +173,8 @@ export class LineChartComponent implements AfterViewInit, OnDestroy, OnChanges { case label.toLowerCase().includes('bits'): units = 'bits'; break; - } - - if (typeof units === 'undefined') { - console.warn('Could not infer units from ' + this.labelY); + default: + console.warn('Could not infer units from ' + this.labelY); } return units; diff --git a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts index 8ae16897ef0..0ff46ebf984 100644 --- a/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts +++ b/src/app/pages/sharing/smb/smb-acl/smb-acl.component.ts @@ -40,14 +40,16 @@ import { TestDirective } from 'app/modules/test-id/test.directive'; import { UserService } from 'app/services/user.service'; import { WebSocketService } from 'app/services/ws.service'; +type NameOrId = string | number | null; + interface FormAclEntry { ae_who_sid: string; ae_who: NfsAclTag.Everyone | NfsAclTag.UserGroup | NfsAclTag.User | NfsAclTag.Both | null; ae_perm: SmbSharesecPermission; ae_type: SmbSharesecType; - user: string | number | null; - group: string | number | null; - both: string | number | null; + user: NameOrId; + group: NameOrId; + both: NameOrId; } @UntilDestroy() diff --git a/src/app/pages/storage/components/dashboard-pool/topology-card/topology-card.component.ts b/src/app/pages/storage/components/dashboard-pool/topology-card/topology-card.component.ts index faa6e91c403..273b24fcc3e 100644 --- a/src/app/pages/storage/components/dashboard-pool/topology-card/topology-card.component.ts +++ b/src/app/pages/storage/components/dashboard-pool/topology-card/topology-card.component.ts @@ -152,13 +152,7 @@ export class TopologyCardComponent implements OnInit, OnChanges { // There should only be one value const allVdevWidths: Set = this.storageService.getVdevWidths(vdevs); const isMixedWidth = this.storageService.isMixedWidth(allVdevWidths); - let isSingleDeviceCategory = false; - - switch (category) { - case VdevType.Spare: - case VdevType.Cache: - isSingleDeviceCategory = true; - } + const isSingleDeviceCategory = [VdevType.Spare, VdevType.Cache].includes(category); if (!isMixedWidth && !isSingleDeviceCategory) { vdevWidth = Array.from(allVdevWidths.values())[0]; diff --git a/src/app/pages/storage/modules/pool-manager/utils/generate-vdevs/generate-vdevs.service.ts b/src/app/pages/storage/modules/pool-manager/utils/generate-vdevs/generate-vdevs.service.ts index 252c6086f43..960d316332a 100644 --- a/src/app/pages/storage/modules/pool-manager/utils/generate-vdevs/generate-vdevs.service.ts +++ b/src/app/pages/storage/modules/pool-manager/utils/generate-vdevs/generate-vdevs.service.ts @@ -145,6 +145,7 @@ export class GenerateVdevsService { let pickedDisk: DetailsDisk; do { + // eslint-disable-next-line @typescript-eslint/no-loop-func pickedDisk = remainingDisks.find((disk) => disk.enclosure?.id === nextEnclosure); if (!pickedDisk) nextEnclosure = this.enclosureList.next(); } while (!pickedDisk); diff --git a/src/app/pages/system/bootenv/bootenv-form/bootenv-form.component.ts b/src/app/pages/system/bootenv/bootenv-form/bootenv-form.component.ts index 81b913c862d..2bd6b6db8cf 100644 --- a/src/app/pages/system/bootenv/bootenv-form/bootenv-form.component.ts +++ b/src/app/pages/system/bootenv/bootenv-form/bootenv-form.component.ts @@ -127,67 +127,75 @@ export class BootEnvironmentFormComponent implements OnInit { onSubmit(): void { this.isFormLoading = true; switch (this.operation) { - case this.Operations.Create: { - const createParams: CreateBootenvParams = [{ - name: this.formGroup.value.name, - }]; - - this.ws.call('bootenv.create', createParams).pipe(untilDestroyed(this)).subscribe({ - next: () => { - this.isFormLoading = false; - this.slideInRef.close(true); - }, - error: (error: unknown) => { - this.isFormLoading = false; - this.slideInRef.close(false); - this.errorHandler.handleWsFormError(error, this.formGroup); - }, - }); - + case this.Operations.Create: + this.createEnvironment(); break; - } - case this.Operations.Rename: { - const renameParams: UpdateBootenvParams = [ - this.currentName, - { - name: this.formGroup.value.name, - }, - ]; - - this.ws.call('bootenv.update', renameParams).pipe(untilDestroyed(this)).subscribe({ - next: () => { - this.isFormLoading = false; - this.slideInRef.close(true); - }, - error: (error: unknown) => { - this.isFormLoading = false; - this.slideInRef.close(false); - this.errorHandler.handleWsFormError(error, this.formGroup); - }, - }); - + case this.Operations.Rename: + this.renameEnvironment(); break; - } - case this.Operations.Clone: { - const cloneParams: CreateBootenvParams = [{ - name: this.formGroup.value.name, - source: this.currentName, - }]; - - this.ws.call('bootenv.create', cloneParams).pipe(untilDestroyed(this)).subscribe({ - next: () => { - this.isFormLoading = false; - this.slideInRef.close(true); - }, - error: (error: unknown) => { - this.isFormLoading = false; - this.slideInRef.close(false); - this.errorHandler.handleWsFormError(error, this.formGroup); - }, - }); - + case this.Operations.Clone: + this.cloneEnvironment(); break; - } + default: + console.error('Unsupported operation'); } } + + private createEnvironment(): void { + const createParams: CreateBootenvParams = [{ + name: this.formGroup.value.name, + }]; + + this.ws.call('bootenv.create', createParams).pipe(untilDestroyed(this)).subscribe({ + next: () => { + this.isFormLoading = false; + this.slideInRef.close(true); + }, + error: (error: unknown) => { + this.isFormLoading = false; + this.slideInRef.close(false); + this.errorHandler.handleWsFormError(error, this.formGroup); + }, + }); + } + + private renameEnvironment(): void { + const renameParams: UpdateBootenvParams = [ + this.currentName, + { + name: this.formGroup.value.name, + }, + ]; + + this.ws.call('bootenv.update', renameParams).pipe(untilDestroyed(this)).subscribe({ + next: () => { + this.isFormLoading = false; + this.slideInRef.close(true); + }, + error: (error: unknown) => { + this.isFormLoading = false; + this.slideInRef.close(false); + this.errorHandler.handleWsFormError(error, this.formGroup); + }, + }); + } + + private cloneEnvironment(): void { + const cloneParams: CreateBootenvParams = [{ + name: this.formGroup.value.name, + source: this.currentName, + }]; + + this.ws.call('bootenv.create', cloneParams).pipe(untilDestroyed(this)).subscribe({ + next: () => { + this.isFormLoading = false; + this.slideInRef.close(true); + }, + error: (error: unknown) => { + this.isFormLoading = false; + this.slideInRef.close(false); + this.errorHandler.handleWsFormError(error, this.formGroup); + }, + }); + } } diff --git a/src/app/pages/system/update/components/update-actions-card/update-actions-card.component.ts b/src/app/pages/system/update/components/update-actions-card/update-actions-card.component.ts index 0b935ac9edb..b51d66de0a1 100644 --- a/src/app/pages/system/update/components/update-actions-card/update-actions-card.component.ts +++ b/src/app/pages/system/update/components/update-actions-card/update-actions-card.component.ts @@ -329,6 +329,7 @@ export class UpdateActionsCardComponent implements OnInit { // Continues the update (based on its type) after the Save Config dialog is closed continueUpdate(): void { + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (this.updateType) { case UpdateType.ApplyPending: { const message = this.isHaLicensed diff --git a/src/app/services/schema/app-schema.transformer.ts b/src/app/services/schema/app-schema.transformer.ts index cdd6b1b2816..802584c8c47 100644 --- a/src/app/services/schema/app-schema.transformer.ts +++ b/src/app/services/schema/app-schema.transformer.ts @@ -28,10 +28,10 @@ const commonSchemaTypes = [ ChartSchemaType.Ipaddr, ChartSchemaType.Uri, ChartSchemaType.Text, -]; +] as const; -export function isCommonSchemaType(type: ChartSchemaType): boolean { - return commonSchemaTypes.includes(type); +export function isCommonSchemaType(type: ChartSchemaType): type is typeof commonSchemaTypes[number] { + return (commonSchemaTypes as unknown as ChartSchemaType[]).includes(type); } export function buildCommonSchemaBase(payload: Partial): CommonSchemaBase {