Skip to content

Commit

Permalink
Merge branch 'master' of github.com:truenas/webui into NAS-133262
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/assets/i18n/ko.json
  • Loading branch information
undsoft committed Dec 25, 2024
2 parents b33cf32 + 0647a78 commit dfa3ef7
Show file tree
Hide file tree
Showing 161 changed files with 1,227 additions and 668 deletions.
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
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import {
createServiceFactory,
mockProvider,
SpectatorService,
} from '@ngneat/spectator/jest';
import { ScheduleDescriptionPipe } from 'app/modules/dates/pipes/schedule-description/schedule-description.pipe';
import { LanguageService } from 'app/services/language.service';
import { LocaleService } from 'app/services/locale.service';

describe('ScheduleDescriptionPipe', () => {
let spectator: SpectatorService<ScheduleDescriptionPipe>;

const createPipe = createServiceFactory({
service: ScheduleDescriptionPipe,
providers: [
mockProvider(LocaleService, {
getShortTimeFormat: () => 'HH:mm',
getPreferredTimeFormat: () => 'HH:mm:ss',
}),
mockProvider(LanguageService, {
currentLanguage: 'en',
}),
],
});

it('describes schedule without `begin` and `end` fields', () => {
spectator = createPipe();

expect(spectator.service.transform({
minute: '0',
hour: '0',
dom: '*',
month: '*',
dow: '*',
})).toBe('At 00:00, every day');

expect(spectator.service.transform({
minute: '0',
hour: '0',
dom: '*',
month: '*',
dow: '1',
})).toBe('At 00:00, only on Monday');

expect(spectator.service.transform({
minute: '0',
hour: '0',
dom: '1',
month: '*',
dow: '*',
})).toBe('At 00:00, on day 1 of the month');

expect(spectator.service.transform({
minute: '0',
hour: '0',
dom: '*',
month: '1',
dow: '*',
})).toBe('At 00:00, every day, only in January');

expect(spectator.service.transform({
minute: '20',
hour: '*/2',
dom: '*',
month: '*',
dow: '1,2,3,4,5',
})).toBe('At 20 minutes past the hour, every 2 hours, only on Monday, Tuesday, Wednesday, Thursday, and Friday');
});

it("uses user's language when describing the schedule", () => {
spectator = createPipe({
providers: [
mockProvider(LanguageService, {
currentLanguage: 'uk',
}),
],
});

expect(spectator.service.transform({
minute: '0',
hour: '0',
dom: '*',
month: '*',
dow: '*',
})).toBe('О 00:00, щоденно');

expect(spectator.service.transform({
minute: '20',
hour: '*/2',
dom: '*',
month: '*',
dow: '1,2',
})).toBe('О 20 хвилині, кожні 2 годин, тільки в понеділок та вівторок');
});

it("uses user's time 12/24h time format preference when describing the schedule", () => {
spectator = createPipe({
providers: [
mockProvider(LocaleService, {
getPreferredTimeFormat: () => 'hh:mm:ss',
}),
],
});

expect(spectator.service.transform({
minute: '45',
hour: '14',
dom: '*',
month: '*',
dow: '*',
})).toBe('At 02:45 PM, every day');
});

it('returns an empty string when invalid schedule is provided and logs console error', () => {
spectator = createPipe();
jest.spyOn(console, 'error').mockImplementation();

expect(spectator.service.transform({})).toBe('');
expect(spectator.service.transform(undefined)).toBe('');
expect(spectator.service.transform(null)).toBe('');

expect(console.error).toHaveBeenCalledTimes(3);
});

describe('when `begin` and `end` are set', () => {
it('describes schedule with `begin` and `end` fields', () => {
spectator = createPipe();

expect(spectator.service.transform({
minute: '0',
hour: '*',
dom: '*',
month: '*',
dow: '*',
begin: '02:15',
end: '23:00',
})).toBe('Every hour, every day, from 02:15 to 23:00');
});

it("uses user's time 12/24h time format preference for schedule with `begin` and `end`", () => {
spectator = createPipe({
providers: [
mockProvider(LocaleService, {
getShortTimeFormat: () => 'hh:mm aa',
getPreferredTimeFormat: () => 'hh:mm:ss aa',
}),
],
});

expect(spectator.service.transform({
minute: '0',
hour: '*',
dom: '*',
month: '*',
dow: '*',
begin: '02:15',
end: '23:00',
})).toBe('Every hour, every day, from 02:15 AM to 11:00 PM');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Pipe, PipeTransform } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import cronstrue from 'cronstrue/i18n';
import { format, parse } from 'date-fns';
import { Schedule } from 'app/interfaces/schedule.interface';
import { scheduleToCrontab } from 'app/modules/scheduler/utils/schedule-to-crontab.utils';
import { LanguageService } from 'app/services/language.service';
import { LocaleService } from 'app/services/locale.service';

@Pipe({
name: 'scheduleDescription',
})
export class ScheduleDescriptionPipe implements PipeTransform {
constructor(
private localeService: LocaleService,
private language: LanguageService,
private translate: TranslateService,
) {}

transform(schedule: Schedule): string {
try {
const crontab = scheduleToCrontab(schedule);
const cronstrueOptions = {
use24HourTimeFormat: this.localeService.getPreferredTimeFormat() === 'HH:mm:ss',
verbose: true,
locale: this.language.currentLanguage,
};

const description = cronstrue.toString(crontab, cronstrueOptions);

if ('begin' in schedule && 'end' in schedule) {
return this.translate.instant('{crontabDescription}, from {startHour} to {endHour}', {
crontabDescription: description,
startHour: this.formatTime(schedule.begin),
endHour: this.formatTime(schedule.end),
});
}

return description;
} catch (error: unknown) {
console.error(error);
return '';
}
}

private formatTime(time: string): string {
const parsedDate = parse(time, 'HH:mm', new Date());

return format(parsedDate, this.localeService.getShortTimeFormat());
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<span
[matTooltip]="value | scheduleDescription"
[ixTest]="[title, uniqueRowTag(row()), 'row-schedule']"
>{{ value | scheduleToCrontab }}</span>
>{{ value | scheduleDescription }}</span>
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Spectator } from '@ngneat/spectator';
import { createComponentFactory } from '@ngneat/spectator/jest';
import { createComponentFactory, mockProvider } from '@ngneat/spectator/jest';
import { Schedule } from 'app/interfaces/schedule.interface';
import { IxCellScheduleComponent } from 'app/modules/ix-table/components/ix-table-body/cells/ix-cell-schedule/ix-cell-schedule.component';
import { LanguageService } from 'app/services/language.service';
import { LocaleService } from 'app/services/locale.service';

interface TestTableData { scheduleField: Schedule }

Expand All @@ -18,6 +20,15 @@ describe('IxCellScheduleComponent', () => {
const createComponent = createComponentFactory({
component: IxCellScheduleComponent<TestTableData>,
detectChanges: false,
providers: [
mockProvider(LocaleService, {
getShortTimeFormat: () => 'HH:mm',
getPreferredTimeFormat: () => 'HH:mm:ss',
}),
mockProvider(LanguageService, {
currentLanguage: 'en',
}),
],
});

beforeEach(() => {
Expand All @@ -31,6 +42,6 @@ describe('IxCellScheduleComponent', () => {
it('shows crontab string when schedule is passed', () => {
spectator.component.setRow({ scheduleField: schedule });
spectator.fixture.detectChanges();
expect(spectator.element.textContent.trim()).toBe('15 10 * 2-5 6');
expect(spectator.element.textContent.trim()).toBe('At 10:15, only on Saturday, February through May');
});
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatTooltip } from '@angular/material/tooltip';
import { ScheduleDescriptionPipe } from 'app/modules/dates/pipes/schedule-description/schedule-description.pipe';
import { ColumnComponent, Column } from 'app/modules/ix-table/interfaces/column-component.class';
import { ScheduleToCrontabPipe } from 'app/modules/pipes/schedule-to-crontab/schedule-to-crontab.pipe';
import { TestDirective } from 'app/modules/test-id/test.directive';

@Component({
selector: 'ix-cell-schedule',
templateUrl: './ix-cell-schedule.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ScheduleToCrontabPipe, TestDirective],
imports: [TestDirective, ScheduleDescriptionPipe, MatTooltip],
})
export class IxCellScheduleComponent<T> extends ColumnComponent<T> {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
ixTest="api-keys"
mat-menu-item
[routerLink]="['/credentials/users/api-keys']"
[queryParams]="{ userName: user.pw_name }">
[queryParams]="{ userName: user.pw_name }"
[ixUiSearch]="searchableElements.elements.myApiKeys"
>
<ix-icon name="laptop"></ix-icon>
{{ 'My API Keys' | translate }}
</a>
Expand Down
4 changes: 4 additions & 0 deletions src/app/modules/layout/topbar/user-menu/user-menu.elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export const userMenuElements = {
guide: {
hierarchy: [T('Guide')],
},
myApiKeys: {
hierarchy: [T('My API Keys')],
synonyms: [T('API Keys')],
},
about: {
hierarchy: [T('About')],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ describe('IxSlideIn2Component', () => {
locked: false,
job: null,
credential: 'test2',
cron_schedule: 'Disabled',
frequency: 'At 00:00, only on Sunday',
next_run_time: 'Disabled',
next_run: 'Disabled',
state: { state: JobState.Pending },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { IxCheckboxComponent } from 'app/modules/forms/ix-forms/components/ix-ch
import { BulkListItemComponent } from 'app/modules/lists/bulk-list-item/bulk-list-item.component';
import { BulkListItem, BulkListItemState } from 'app/modules/lists/bulk-list-item/bulk-list-item.interface';
import { TestDirective } from 'app/modules/test-id/test.directive';
import { ErrorHandlerService } from 'app/services/error-handler.service';
import { ApiService } from 'app/services/websocket/api.service';

@UntilDestroy()
Expand Down Expand Up @@ -73,6 +74,7 @@ export class DockerImageDeleteDialogComponent {
private fb: FormBuilder,
private api: ApiService,
private cdr: ChangeDetectorRef,
private errorHandler: ErrorHandlerService,
private dialogRef: MatDialogRef<DockerImageDeleteDialogComponent>,
@Inject(MAT_DIALOG_DATA) public images: ContainerImage[],
) {
Expand All @@ -92,6 +94,7 @@ export class DockerImageDeleteDialogComponent {

this.api.job('core.bulk', ['app.image.delete', deleteParams]).pipe(
filter((job: Job<CoreBulkResponse<void>[], DeleteContainerImageParams[]>) => !!job.result),
this.errorHandler.catchError(),
untilDestroyed(this),
).subscribe((response) => {
response.arguments[1].forEach((params, index: number) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { FakeProgressBarComponent } from 'app/modules/loader/components/fake-pro
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { TestDirective } from 'app/modules/test-id/test.directive';
import { ApplicationsService } from 'app/pages/apps/services/applications.service';
import { ErrorHandlerService } from 'app/services/error-handler.service';
import { ApiService } from 'app/services/websocket/api.service';

@UntilDestroy()
Expand Down Expand Up @@ -83,6 +84,7 @@ export class AppBulkUpgradeComponent {
private dialogRef: MatDialogRef<AppBulkUpgradeComponent>,
private appService: ApplicationsService,
private snackbar: SnackbarService,
private errorHandler: ErrorHandlerService,
@Inject(MAT_DIALOG_DATA) private apps: App[],
) {
this.apps = this.apps.filter((app) => app.upgrade_available);
Expand Down Expand Up @@ -152,7 +154,10 @@ export class AppBulkUpgradeComponent {

this.api
.job('core.bulk', ['app.upgrade', payload])
.pipe(untilDestroyed(this))
.pipe(
this.errorHandler.catchError(),
untilDestroyed(this),
)
.subscribe(() => {
this.dialogRef.close();
this.snackbar.success(
Expand Down
Loading

0 comments on commit dfa3ef7

Please sign in to comment.