mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-06-10 06:45:35 -04:00
added queue cleaner settings
This commit is contained in:
208
code/UI/src/app/core/services/configuration.service.ts
Normal file
208
code/UI/src/app/core/services/configuration.service.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable, catchError, map, throwError } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { JobSchedule, QueueCleanerConfig, ScheduleUnit } from '../../shared/models/queue-cleaner-config.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ConfigurationService {
|
||||
private readonly apiUrl = environment.apiUrl;
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
/**
|
||||
* Get queue cleaner configuration
|
||||
*/
|
||||
getQueueCleanerConfig(): Observable<QueueCleanerConfig> {
|
||||
return this.http.get<QueueCleanerConfig>(`${this.apiUrl}/api/configuration/queue_cleaner`)
|
||||
.pipe(
|
||||
map(response => this.transformQueueCleanerResponse(response)),
|
||||
catchError(error => {
|
||||
console.error('Error fetching queue cleaner config:', error);
|
||||
return throwError(() => new Error('Failed to load queue cleaner configuration'));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update queue cleaner configuration
|
||||
*/
|
||||
updateQueueCleanerConfig(config: QueueCleanerConfig): Observable<any> {
|
||||
// Create a copy to avoid modifying the original
|
||||
const configToSend = this.prepareQueueCleanerConfigForSending({ ...config });
|
||||
|
||||
return this.http.put<any>(`${this.apiUrl}/api/configuration/queue_cleaner`, configToSend)
|
||||
.pipe(
|
||||
catchError(error => {
|
||||
console.error('Error updating queue cleaner config:', error);
|
||||
return throwError(() => new Error(error.error?.error || 'Failed to update queue cleaner configuration'));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the API response to our frontend model
|
||||
* Convert property names from PascalCase to camelCase
|
||||
*/
|
||||
private transformQueueCleanerResponse(response: any): QueueCleanerConfig {
|
||||
const config: QueueCleanerConfig = {
|
||||
enabled: response.Enabled,
|
||||
cronExpression: response.CronExpression,
|
||||
runSequentially: response.RunSequentially,
|
||||
ignoredDownloadsPath: response.IgnoredDownloadsPath || '',
|
||||
|
||||
// Failed Import settings
|
||||
failedImportMaxStrikes: response.FailedImportMaxStrikes,
|
||||
failedImportIgnorePrivate: response.FailedImportIgnorePrivate,
|
||||
failedImportDeletePrivate: response.FailedImportDeletePrivate,
|
||||
failedImportIgnorePatterns: response.FailedImportIgnorePatterns || [],
|
||||
|
||||
// Stalled settings
|
||||
stalledMaxStrikes: response.StalledMaxStrikes,
|
||||
stalledResetStrikesOnProgress: response.StalledResetStrikesOnProgress,
|
||||
stalledIgnorePrivate: response.StalledIgnorePrivate,
|
||||
stalledDeletePrivate: response.StalledDeletePrivate,
|
||||
|
||||
// Downloading Metadata settings
|
||||
downloadingMetadataMaxStrikes: response.DownloadingMetadataMaxStrikes,
|
||||
|
||||
// Slow Download settings
|
||||
slowMaxStrikes: response.SlowMaxStrikes,
|
||||
slowResetStrikesOnProgress: response.SlowResetStrikesOnProgress,
|
||||
slowIgnorePrivate: response.SlowIgnorePrivate,
|
||||
slowDeletePrivate: response.SlowDeletePrivate,
|
||||
slowMinSpeed: response.SlowMinSpeed || '',
|
||||
slowMaxTime: response.SlowMaxTime,
|
||||
slowIgnoreAboveSize: response.SlowIgnoreAboveSize || '',
|
||||
};
|
||||
|
||||
// Attempt to extract job schedule from cron expression
|
||||
// This is just UI sugar, not sent back to API
|
||||
config.jobSchedule = this.tryExtractJobScheduleFromCron(config.cronExpression);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare configuration object for sending to API
|
||||
* Convert property names from camelCase to PascalCase
|
||||
*/
|
||||
private prepareQueueCleanerConfigForSending(config: QueueCleanerConfig): any {
|
||||
// If we have a job schedule, update the cron expression
|
||||
if (config.jobSchedule) {
|
||||
config.cronExpression = this.convertJobScheduleToCron(config.jobSchedule);
|
||||
}
|
||||
|
||||
// Remove UI-only properties
|
||||
const { jobSchedule, ...rest } = config;
|
||||
|
||||
// Convert to PascalCase for backend
|
||||
return {
|
||||
Enabled: rest.enabled,
|
||||
CronExpression: rest.cronExpression,
|
||||
RunSequentially: rest.runSequentially,
|
||||
IgnoredDownloadsPath: rest.ignoredDownloadsPath,
|
||||
|
||||
// Failed Import settings
|
||||
FailedImportMaxStrikes: rest.failedImportMaxStrikes,
|
||||
FailedImportIgnorePrivate: rest.failedImportIgnorePrivate,
|
||||
FailedImportDeletePrivate: rest.failedImportDeletePrivate,
|
||||
FailedImportIgnorePatterns: rest.failedImportIgnorePatterns,
|
||||
|
||||
// Stalled settings
|
||||
StalledMaxStrikes: rest.stalledMaxStrikes,
|
||||
StalledResetStrikesOnProgress: rest.stalledResetStrikesOnProgress,
|
||||
StalledIgnorePrivate: rest.stalledIgnorePrivate,
|
||||
StalledDeletePrivate: rest.stalledDeletePrivate,
|
||||
|
||||
// Downloading Metadata settings
|
||||
DownloadingMetadataMaxStrikes: rest.downloadingMetadataMaxStrikes,
|
||||
|
||||
// Slow Download settings
|
||||
SlowMaxStrikes: rest.slowMaxStrikes,
|
||||
SlowResetStrikesOnProgress: rest.slowResetStrikesOnProgress,
|
||||
SlowIgnorePrivate: rest.slowIgnorePrivate,
|
||||
SlowDeletePrivate: rest.slowDeletePrivate,
|
||||
SlowMinSpeed: rest.slowMinSpeed,
|
||||
SlowMaxTime: rest.slowMaxTime,
|
||||
SlowIgnoreAboveSize: rest.slowIgnoreAboveSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to extract a JobSchedule from a cron expression
|
||||
* Only handles the simple cases we're generating
|
||||
*/
|
||||
private tryExtractJobScheduleFromCron(cronExpression: string): JobSchedule | undefined {
|
||||
// Patterns we support:
|
||||
// Seconds: */n * * ? * * *
|
||||
// Minutes: 0 */n * ? * * *
|
||||
// Hours: 0 0 */n ? * * *
|
||||
try {
|
||||
const parts = cronExpression.split(' ');
|
||||
|
||||
if (parts.length !== 7) return undefined;
|
||||
|
||||
// Every n seconds
|
||||
if (parts[0].startsWith('*/') && parts[1] === '*') {
|
||||
const seconds = parseInt(parts[0].substring(2));
|
||||
if (!isNaN(seconds) && seconds > 0 && seconds < 60) {
|
||||
return { every: seconds, type: ScheduleUnit.Seconds };
|
||||
}
|
||||
}
|
||||
|
||||
// Every n minutes
|
||||
if (parts[0] === '0' && parts[1].startsWith('*/')) {
|
||||
const minutes = parseInt(parts[1].substring(2));
|
||||
if (!isNaN(minutes) && minutes > 0 && minutes < 60) {
|
||||
return { every: minutes, type: ScheduleUnit.Minutes };
|
||||
}
|
||||
}
|
||||
|
||||
// Every n hours
|
||||
if (parts[0] === '0' && parts[1] === '0' && parts[2].startsWith('*/')) {
|
||||
const hours = parseInt(parts[2].substring(2));
|
||||
if (!isNaN(hours) && hours > 0 && hours < 24) {
|
||||
return { every: hours, type: ScheduleUnit.Hours };
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not parse cron expression:', cronExpression);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a JobSchedule to a cron expression
|
||||
*/
|
||||
private convertJobScheduleToCron(schedule: JobSchedule): string {
|
||||
if (!schedule || schedule.every <= 0) {
|
||||
return '0 0/5 * * * ?'; // Default: every 5 minutes
|
||||
}
|
||||
|
||||
switch (schedule.type) {
|
||||
case ScheduleUnit.Seconds:
|
||||
if (schedule.every < 60) {
|
||||
return `*/${schedule.every} * * ? * * *`;
|
||||
}
|
||||
break;
|
||||
|
||||
case ScheduleUnit.Minutes:
|
||||
if (schedule.every < 60) {
|
||||
return `0 */${schedule.every} * ? * * *`;
|
||||
}
|
||||
break;
|
||||
|
||||
case ScheduleUnit.Hours:
|
||||
if (schedule.every < 24) {
|
||||
return `0 0 */${schedule.every} ? * * *`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Fallback to default
|
||||
return '0 0/5 * * * ?';
|
||||
}
|
||||
}
|
||||
@@ -1,148 +1,260 @@
|
||||
<div class="settings-container">
|
||||
<div class="flex align-items-center justify-content-between mb-4">
|
||||
<!-- Toast for notifications -->
|
||||
<p-toast></p-toast>
|
||||
|
||||
<div class="settings-header">
|
||||
<h1>Settings</h1>
|
||||
<div>
|
||||
<button pButton label="Save Changes" icon="pi pi-save" class="p-button-success mr-2" (click)="saveSettings()"></button>
|
||||
<button pButton label="Reset to Defaults" icon="pi pi-refresh" class="p-button-secondary p-button-outlined" (click)="resetToDefaults()"></button>
|
||||
</div>
|
||||
<p>Manage application settings and configurations</p>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-6">
|
||||
<p-card header="General Settings" styleClass="settings-card mb-4">
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">API Connection</h3>
|
||||
|
||||
<div class="field-row">
|
||||
<label for="apiUrl">API URL</label>
|
||||
<div class="field-input">
|
||||
<span class="p-input-icon-left w-full">
|
||||
<i class="pi pi-link"></i>
|
||||
<input id="apiUrl" type="text" pInputText [(ngModel)]="apiUrl" class="w-full" placeholder="https://api.example.com" />
|
||||
</span>
|
||||
<small class="form-helper-text">The base URL of the Cleanuparr API service</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label for="apiKey">API Key</label>
|
||||
<div class="field-input">
|
||||
<div class="p-inputgroup">
|
||||
<input id="apiKey" type="password" pInputText [(ngModel)]="apiKey" class="w-full" placeholder="Enter API key" />
|
||||
<button type="button" pButton icon="pi pi-eye" class="p-button-outlined"></button>
|
||||
</div>
|
||||
<small class="form-helper-text">Required for authenticated API requests</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label for="apiTimeout">Timeout (seconds)</label>
|
||||
<div class="field-input">
|
||||
<p-inputNumber id="apiTimeout" [(ngModel)]="apiTimeout" [min]="5" [max]="120" [showButtons]="true" suffix=" sec" buttonLayout="horizontal" spinnerMode="horizontal"
|
||||
inputStyleClass="text-right" [step]="5" decrementButtonClass="p-button-secondary" incrementButtonClass="p-button-secondary"></p-inputNumber>
|
||||
<small class="form-helper-text">Maximum time to wait for API responses</small>
|
||||
</div>
|
||||
<!-- Queue Cleaner Settings Card -->
|
||||
<div class="col-12 mb-4">
|
||||
<p-card header="Queue Cleaner Configuration" styleClass="settings-card">
|
||||
<form [formGroup]="queueCleanerForm" class="p-fluid">
|
||||
<!-- Main Settings -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">Enable Queue Cleaner</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="enabled" [binary]="true" inputId="qcEnabled"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, the queue cleaner will run according to the schedule</small>
|
||||
</div>
|
||||
</div>
|
||||
</p-card>
|
||||
|
||||
<p-card header="UI Preferences" styleClass="settings-card">
|
||||
<div class="settings-section">
|
||||
<div class="field-row align-items-center">
|
||||
<label for="theme">Theme</label>
|
||||
<div class="field-input">
|
||||
<div class="flex gap-3 align-items-center">
|
||||
<p-radioButton id="themeLight" name="theme" value="light" [(ngModel)]="theme" inputId="light"></p-radioButton>
|
||||
<label for="light" class="mr-5">Light</label>
|
||||
|
||||
<p-radioButton id="themeDark" name="theme" value="dark" [(ngModel)]="theme" inputId="dark"></p-radioButton>
|
||||
<label for="dark">Dark</label>
|
||||
|
||||
<div class="field-row" formGroupName="jobSchedule">
|
||||
<label class="field-label">Run Schedule</label>
|
||||
<div class="field-input schedule-input">
|
||||
<span class="schedule-label">Every</span>
|
||||
<p-inputNumber
|
||||
formControlName="every"
|
||||
[showButtons]="true"
|
||||
[min]="1"
|
||||
[max]="59"
|
||||
buttonLayout="horizontal"
|
||||
inputStyleClass="schedule-value"
|
||||
[disabled]="!queueCleanerForm.get('enabled')?.value">
|
||||
</p-inputNumber>
|
||||
|
||||
<p-selectButton
|
||||
formControlName="type"
|
||||
[options]="scheduleUnitOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
[disabled]="!queueCleanerForm.get('enabled')?.value">
|
||||
</p-selectButton>
|
||||
</div>
|
||||
<small class="form-helper-text">How often the queue cleaner should run</small>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Run Sequentially</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="runSequentially" [binary]="true" inputId="qcRunSequentially" [disabled]="!queueCleanerForm.get('enabled')?.value"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, jobs will run one after another instead of in parallel</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Ignored Downloads Path</label>
|
||||
<div class="field-input">
|
||||
<input pInputText formControlName="ignoredDownloadsPath" [disabled]="!queueCleanerForm.get('enabled')?.value" />
|
||||
<small class="form-helper-text">Path to the file containing ignored downloads</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Settings in Accordion -->
|
||||
<p-accordion [multiple]="true" styleClass="mt-3">
|
||||
<!-- Failed Import Settings -->
|
||||
<p-accordionTab header="Failed Import Settings" [disabled]="!queueCleanerForm.get('enabled')?.value">
|
||||
<div class="field-row">
|
||||
<label class="field-label">Max Strikes</label>
|
||||
<div class="field-input">
|
||||
<p-inputNumber
|
||||
formControlName="failedImportMaxStrikes"
|
||||
[showButtons]="true"
|
||||
[min]="0"
|
||||
[max]="10"
|
||||
buttonLayout="horizontal">
|
||||
</p-inputNumber>
|
||||
<small class="form-helper-text">Number of strikes before action is taken (0 to disable, min 3 to enable)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row align-items-center">
|
||||
<label for="fontSize">UI Font Size</label>
|
||||
<div class="field-input">
|
||||
<p-slider [(ngModel)]="fontSize" [min]="12" [max]="18" class="w-full mb-3"></p-slider>
|
||||
<div class="text-center font-medium">{{ fontSize }}px</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Ignore Private</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="failedImportIgnorePrivate" [binary]="true" [disabled]="queueCleanerForm.get('failedImportMaxStrikes')?.value < 3"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be ignored</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Delete Private</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="failedImportDeletePrivate" [binary]="true" [disabled]="queueCleanerForm.get('failedImportMaxStrikes')?.value < 3"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Ignore Patterns</label>
|
||||
<div class="field-input">
|
||||
<p-chips formControlName="failedImportIgnorePatterns" [disabled]="queueCleanerForm.get('failedImportMaxStrikes')?.value < 3" placeholder="Add pattern and press Enter"></p-chips>
|
||||
<small class="form-helper-text">Patterns to ignore (e.g., *sample*)</small>
|
||||
</div>
|
||||
</div>
|
||||
</p-accordionTab>
|
||||
|
||||
<!-- Stalled Settings -->
|
||||
<p-accordionTab header="Stalled Download Settings" [disabled]="!queueCleanerForm.get('enabled')?.value">
|
||||
<div class="field-row">
|
||||
<label class="field-label">Max Strikes</label>
|
||||
<div class="field-input">
|
||||
<p-inputNumber
|
||||
formControlName="stalledMaxStrikes"
|
||||
[showButtons]="true"
|
||||
[min]="0"
|
||||
[max]="10"
|
||||
buttonLayout="horizontal">
|
||||
</p-inputNumber>
|
||||
<small class="form-helper-text">Number of strikes before action is taken (0 to disable, min 3 to enable)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Reset Strikes On Progress</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="stalledResetStrikesOnProgress" [binary]="true" [disabled]="queueCleanerForm.get('stalledMaxStrikes')?.value < 3"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, strikes will be reset if download progress is made</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Ignore Private</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="stalledIgnorePrivate" [binary]="true" [disabled]="queueCleanerForm.get('stalledMaxStrikes')?.value < 3"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be ignored</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Delete Private</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="stalledDeletePrivate" [binary]="true" [disabled]="queueCleanerForm.get('stalledMaxStrikes')?.value < 3"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
|
||||
</div>
|
||||
</div>
|
||||
</p-accordionTab>
|
||||
|
||||
<!-- Downloading Metadata Settings -->
|
||||
<p-accordionTab header="Downloading Metadata Settings" [disabled]="!queueCleanerForm.get('enabled')?.value">
|
||||
<div class="field-row">
|
||||
<label class="field-label">Max Strikes</label>
|
||||
<div class="field-input">
|
||||
<p-inputNumber
|
||||
formControlName="downloadingMetadataMaxStrikes"
|
||||
[showButtons]="true"
|
||||
[min]="0"
|
||||
[max]="10"
|
||||
buttonLayout="horizontal">
|
||||
</p-inputNumber>
|
||||
<small class="form-helper-text">Number of strikes before action is taken (0 to disable, min 3 to enable)</small>
|
||||
</div>
|
||||
</div>
|
||||
</p-accordionTab>
|
||||
|
||||
<!-- Slow Download Settings -->
|
||||
<p-accordionTab header="Slow Download Settings" [disabled]="!queueCleanerForm.get('enabled')?.value">
|
||||
<div class="field-row">
|
||||
<label class="field-label">Max Strikes</label>
|
||||
<div class="field-input">
|
||||
<p-inputNumber
|
||||
formControlName="slowMaxStrikes"
|
||||
[showButtons]="true"
|
||||
[min]="0"
|
||||
[max]="10"
|
||||
buttonLayout="horizontal">
|
||||
</p-inputNumber>
|
||||
<small class="form-helper-text">Number of strikes before action is taken (0 to disable, min 3 to enable)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Reset Strikes On Progress</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="slowResetStrikesOnProgress" [binary]="true" [disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, strikes will be reset if download progress is made</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Ignore Private</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="slowIgnorePrivate" [binary]="true" [disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be ignored</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Delete Private</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="slowDeletePrivate" [binary]="true" [disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Minimum Speed</label>
|
||||
<div class="field-input">
|
||||
<app-byte-size-input
|
||||
formControlName="slowMinSpeed"
|
||||
[disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"
|
||||
[min]="0"
|
||||
placeholder="Enter minimum speed"
|
||||
helpText="Minimum speed threshold for slow downloads (e.g., 100KB/s)">
|
||||
</app-byte-size-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Maximum Time (hours)</label>
|
||||
<div class="field-input">
|
||||
<p-inputNumber
|
||||
formControlName="slowMaxTime"
|
||||
[showButtons]="true"
|
||||
[min]="0"
|
||||
[max]="168"
|
||||
buttonLayout="horizontal"
|
||||
[disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3">
|
||||
</p-inputNumber>
|
||||
<small class="form-helper-text">Maximum time allowed for slow downloads (in hours)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">Ignore Above Size</label>
|
||||
<div class="field-input">
|
||||
<app-byte-size-input
|
||||
formControlName="slowIgnoreAboveSize"
|
||||
[disabled]="queueCleanerForm.get('slowMaxStrikes')?.value < 3"
|
||||
[min]="0"
|
||||
placeholder="Enter size threshold"
|
||||
helpText="Size threshold above which slow downloads are ignored">
|
||||
</app-byte-size-input>
|
||||
</div>
|
||||
</div>
|
||||
</p-accordionTab>
|
||||
</p-accordion>
|
||||
|
||||
<!-- Save & Reset buttons -->
|
||||
<div class="form-actions mt-4">
|
||||
<button pButton type="button" label="Save" icon="pi pi-save" class="p-button-success"
|
||||
[disabled]="queueCleanerForm.pristine || queueCleanerSaving()"
|
||||
[loading]="queueCleanerSaving()"
|
||||
(click)="saveQueueCleanerConfig()"></button>
|
||||
<button pButton type="button" label="Reset" icon="pi pi-refresh" class="p-button-secondary p-button-outlined ml-2"
|
||||
(click)="resetQueueCleanerConfig()"></button>
|
||||
</div>
|
||||
</p-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6">
|
||||
<p-card header="Logging Configuration" styleClass="settings-card mb-4">
|
||||
<div class="settings-section">
|
||||
<div class="field-row">
|
||||
<label for="enableLogs">Enable Logging</label>
|
||||
<div class="field-input">
|
||||
<p-inputSwitch id="enableLogs" [(ngModel)]="enableLogs"></p-inputSwitch>
|
||||
<small class="form-helper-text">When enabled, application events will be logged</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row" [class.field-disabled]="!enableLogs">
|
||||
<label for="logLevel">Log Level</label>
|
||||
<div class="field-input">
|
||||
<p-dropdown id="logLevel" [options]="logLevels" [(ngModel)]="logLevel" optionLabel="label" [disabled]="!enableLogs"
|
||||
placeholder="Select log level" [showClear]="false" styleClass="w-full">
|
||||
<ng-template pTemplate="selectedItem">
|
||||
<div class="flex align-items-center gap-2" *ngIf="logLevel">
|
||||
<p-tag [severity]="getSeverity(logLevel.value)" [value]="logLevel.label"></p-tag>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template let-level pTemplate="item">
|
||||
<p-tag [severity]="getSeverity(level.value)" [value]="level.label"></p-tag>
|
||||
</ng-template>
|
||||
</p-dropdown>
|
||||
<small class="form-helper-text">Minimum level of logs to capture</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row" [class.field-disabled]="!enableLogs">
|
||||
<label for="enableNotifications">Show Notifications</label>
|
||||
<div class="field-input">
|
||||
<p-inputSwitch id="enableNotifications" [(ngModel)]="enableNotifications" [disabled]="!enableLogs"></p-inputSwitch>
|
||||
<small class="form-helper-text">Show desktop notifications for important logs</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p-card>
|
||||
|
||||
<p-card header="Log Viewer Settings" styleClass="settings-card">
|
||||
<div class="settings-section">
|
||||
<div class="field-row">
|
||||
<label for="autoRefresh">Auto-refresh Logs</label>
|
||||
<div class="field-input">
|
||||
<p-inputSwitch id="autoRefresh" [(ngModel)]="autoRefresh"></p-inputSwitch>
|
||||
<small class="form-helper-text">Automatically refresh logs at the specified interval</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row" [class.field-disabled]="!autoRefresh">
|
||||
<label for="refreshInterval">Refresh Interval</label>
|
||||
<div class="field-input">
|
||||
<p-slider [(ngModel)]="refreshInterval" [min]="5" [max]="60" [disabled]="!autoRefresh" class="w-full mb-3"></p-slider>
|
||||
<div class="text-center font-medium">{{ refreshInterval }} seconds</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label for="maxLogEntries">Max Log Entries</label>
|
||||
<div class="field-input">
|
||||
<p-dropdown id="maxLogEntries" [options]="maxLogOptions" [(ngModel)]="maxLogEntries"
|
||||
optionLabel="label" optionValue="value" styleClass="w-full"></p-dropdown>
|
||||
<small class="form-helper-text">Maximum number of log entries to display at once</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-0 right-0 p-3" *ngIf="showSaveNotification">
|
||||
<p-toast position="bottom-right" key="settings"></p-toast>
|
||||
</form>
|
||||
</p-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, computed, inject, effect } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from 'primeng/card';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { InputSwitchModule } from 'primeng/inputswitch';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { DropdownModule } from 'primeng/dropdown';
|
||||
import { SliderModule } from 'primeng/slider';
|
||||
import { RadioButtonModule } from 'primeng/radiobutton';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { InputNumberModule } from 'primeng/inputnumber';
|
||||
import { TagModule } from 'primeng/tag';
|
||||
import { DropdownModule } from 'primeng/dropdown';
|
||||
import { PasswordModule } from 'primeng/password';
|
||||
import { ToastModule } from 'primeng/toast';
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { CheckboxModule } from 'primeng/checkbox';
|
||||
import { AccordionModule } from 'primeng/accordion';
|
||||
import { SelectButtonModule } from 'primeng/selectbutton';
|
||||
import { ChipsModule } from 'primeng/chips';
|
||||
import { TagModule } from 'primeng/tag';
|
||||
import { SliderModule } from 'primeng/slider';
|
||||
import { RadioButtonModule } from 'primeng/radiobutton';
|
||||
import { DividerModule } from 'primeng/divider';
|
||||
import { InputGroupModule } from 'primeng/inputgroup';
|
||||
import { InputGroupAddonModule } from 'primeng/inputgroupaddon';
|
||||
|
||||
// Custom Components and Services
|
||||
import { ByteSizeInputComponent } from '../../shared/components/byte-size-input/byte-size-input.component';
|
||||
import { QueueCleanerConfigStore } from '../store/queue-cleaner-config.store';
|
||||
import { QueueCleanerConfig, ScheduleUnit } from '../../shared/models/queue-cleaner-config.model';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
|
||||
// Define interfaces for our settings
|
||||
interface LogLevel {
|
||||
@@ -33,18 +46,25 @@ interface MaxLogOption {
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CardModule,
|
||||
InputTextModule,
|
||||
InputSwitchModule,
|
||||
CheckboxModule,
|
||||
ButtonModule,
|
||||
InputNumberModule,
|
||||
DropdownModule,
|
||||
PasswordModule,
|
||||
ToastModule,
|
||||
TagModule,
|
||||
SliderModule,
|
||||
RadioButtonModule,
|
||||
InputNumberModule,
|
||||
TagModule,
|
||||
ToastModule
|
||||
AccordionModule,
|
||||
DividerModule,
|
||||
SelectButtonModule,
|
||||
ChipsModule,
|
||||
ByteSizeInputComponent
|
||||
],
|
||||
providers: [MessageService],
|
||||
providers: [MessageService, QueueCleanerConfigStore],
|
||||
templateUrl: './settings-page.component.html',
|
||||
styleUrl: './settings-page.component.scss'
|
||||
})
|
||||
@@ -84,15 +104,117 @@ export class SettingsPageComponent implements OnInit {
|
||||
{ label: '500 entries', value: 500 },
|
||||
{ label: '1000 entries', value: 1000 }
|
||||
];
|
||||
|
||||
// Schedule unit options for job schedules
|
||||
scheduleUnitOptions = [
|
||||
{ label: 'Seconds', value: ScheduleUnit.Seconds },
|
||||
{ label: 'Minutes', value: ScheduleUnit.Minutes },
|
||||
{ label: 'Hours', value: ScheduleUnit.Hours }
|
||||
];
|
||||
|
||||
// UI state
|
||||
showSaveNotification = false;
|
||||
|
||||
// Queue Cleaner Configuration Form
|
||||
queueCleanerForm: FormGroup;
|
||||
|
||||
constructor(private messageService: MessageService) {}
|
||||
// Inject the necessary services
|
||||
private formBuilder = inject(FormBuilder);
|
||||
private messageService = inject(MessageService);
|
||||
private queueCleanerStore = inject(QueueCleanerConfigStore);
|
||||
|
||||
// Signals from the store
|
||||
readonly queueCleanerConfig = this.queueCleanerStore.config;
|
||||
readonly queueCleanerLoading = this.queueCleanerStore.loading;
|
||||
readonly queueCleanerSaving = this.queueCleanerStore.saving;
|
||||
readonly queueCleanerError = this.queueCleanerStore.error;
|
||||
|
||||
constructor() {
|
||||
// Initialize the queue cleaner form
|
||||
this.queueCleanerForm = this.formBuilder.group({
|
||||
enabled: [false],
|
||||
jobSchedule: this.formBuilder.group({
|
||||
every: [5, [Validators.required, Validators.min(1)]],
|
||||
type: [ScheduleUnit.Minutes]
|
||||
}),
|
||||
runSequentially: [false],
|
||||
ignoredDownloadsPath: [''],
|
||||
|
||||
// Failed Import settings
|
||||
failedImportMaxStrikes: [0, [Validators.min(0)]],
|
||||
failedImportIgnorePrivate: [false],
|
||||
failedImportDeletePrivate: [false],
|
||||
failedImportIgnorePatterns: [[]],
|
||||
|
||||
// Stalled settings
|
||||
stalledMaxStrikes: [0, [Validators.min(0)]],
|
||||
stalledResetStrikesOnProgress: [false],
|
||||
stalledIgnorePrivate: [false],
|
||||
stalledDeletePrivate: [false],
|
||||
|
||||
// Downloading Metadata settings
|
||||
downloadingMetadataMaxStrikes: [0, [Validators.min(0)]],
|
||||
|
||||
// Slow Download settings
|
||||
slowMaxStrikes: [0, [Validators.min(0)]],
|
||||
slowResetStrikesOnProgress: [false],
|
||||
slowIgnorePrivate: [false],
|
||||
slowDeletePrivate: [false],
|
||||
slowMinSpeed: [''],
|
||||
slowMaxTime: [0, [Validators.min(0)]],
|
||||
slowIgnoreAboveSize: ['']
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Load saved settings if available
|
||||
this.loadSettings();
|
||||
|
||||
// The QueueCleanerConfigStore automatically loads the configuration when initialized
|
||||
// Create an effect to update the form when the configuration changes
|
||||
effect(() => {
|
||||
const config = this.queueCleanerConfig();
|
||||
if (config) {
|
||||
// Update the form with the current configuration
|
||||
this.queueCleanerForm.patchValue({
|
||||
enabled: config.enabled,
|
||||
runSequentially: config.runSequentially,
|
||||
ignoredDownloadsPath: config.ignoredDownloadsPath,
|
||||
|
||||
// Failed Import settings
|
||||
failedImportMaxStrikes: config.failedImportMaxStrikes,
|
||||
failedImportIgnorePrivate: config.failedImportIgnorePrivate,
|
||||
failedImportDeletePrivate: config.failedImportDeletePrivate,
|
||||
failedImportIgnorePatterns: config.failedImportIgnorePatterns,
|
||||
|
||||
// Stalled settings
|
||||
stalledMaxStrikes: config.stalledMaxStrikes,
|
||||
stalledResetStrikesOnProgress: config.stalledResetStrikesOnProgress,
|
||||
stalledIgnorePrivate: config.stalledIgnorePrivate,
|
||||
stalledDeletePrivate: config.stalledDeletePrivate,
|
||||
|
||||
// Downloading Metadata settings
|
||||
downloadingMetadataMaxStrikes: config.downloadingMetadataMaxStrikes,
|
||||
|
||||
// Slow Download settings
|
||||
slowMaxStrikes: config.slowMaxStrikes,
|
||||
slowResetStrikesOnProgress: config.slowResetStrikesOnProgress,
|
||||
slowIgnorePrivate: config.slowIgnorePrivate,
|
||||
slowDeletePrivate: config.slowDeletePrivate,
|
||||
slowMinSpeed: config.slowMinSpeed,
|
||||
slowMaxTime: config.slowMaxTime,
|
||||
slowIgnoreAboveSize: config.slowIgnoreAboveSize
|
||||
});
|
||||
|
||||
// Update job schedule if it exists
|
||||
if (config.jobSchedule) {
|
||||
this.queueCleanerForm.get('jobSchedule')?.patchValue({
|
||||
every: config.jobSchedule.every,
|
||||
type: config.jobSchedule.type
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSeverity(level: string): string {
|
||||
@@ -185,4 +307,134 @@ export class SettingsPageComponent implements OnInit {
|
||||
resetApiUrl(): void {
|
||||
this.apiUrl = environment.apiUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the queue cleaner configuration
|
||||
*/
|
||||
saveQueueCleanerConfig(): void {
|
||||
if (this.queueCleanerForm.invalid) {
|
||||
// Mark all fields as touched to show validation errors
|
||||
this.markFormGroupTouched(this.queueCleanerForm);
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Validation Error',
|
||||
detail: 'Please correct the errors in the form before saving.',
|
||||
life: 5000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the form values
|
||||
const formValues = this.queueCleanerForm.value;
|
||||
|
||||
// Build the configuration object
|
||||
const config: QueueCleanerConfig = {
|
||||
enabled: formValues.enabled,
|
||||
// The cronExpression will be generated from the jobSchedule when saving
|
||||
cronExpression: '',
|
||||
jobSchedule: formValues.jobSchedule,
|
||||
runSequentially: formValues.runSequentially,
|
||||
ignoredDownloadsPath: formValues.ignoredDownloadsPath || '',
|
||||
|
||||
// Failed Import settings
|
||||
failedImportMaxStrikes: formValues.failedImportMaxStrikes,
|
||||
failedImportIgnorePrivate: formValues.failedImportIgnorePrivate,
|
||||
failedImportDeletePrivate: formValues.failedImportDeletePrivate,
|
||||
failedImportIgnorePatterns: formValues.failedImportIgnorePatterns || [],
|
||||
|
||||
// Stalled settings
|
||||
stalledMaxStrikes: formValues.stalledMaxStrikes,
|
||||
stalledResetStrikesOnProgress: formValues.stalledResetStrikesOnProgress,
|
||||
stalledIgnorePrivate: formValues.stalledIgnorePrivate,
|
||||
stalledDeletePrivate: formValues.stalledDeletePrivate,
|
||||
|
||||
// Downloading Metadata settings
|
||||
downloadingMetadataMaxStrikes: formValues.downloadingMetadataMaxStrikes,
|
||||
|
||||
// Slow Download settings
|
||||
slowMaxStrikes: formValues.slowMaxStrikes,
|
||||
slowResetStrikesOnProgress: formValues.slowResetStrikesOnProgress,
|
||||
slowIgnorePrivate: formValues.slowIgnorePrivate,
|
||||
slowDeletePrivate: formValues.slowDeletePrivate,
|
||||
slowMinSpeed: formValues.slowMinSpeed || '',
|
||||
slowMaxTime: formValues.slowMaxTime,
|
||||
slowIgnoreAboveSize: formValues.slowIgnoreAboveSize || ''
|
||||
};
|
||||
|
||||
// Save the configuration
|
||||
this.queueCleanerStore.saveConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the queue cleaner configuration form to default values
|
||||
*/
|
||||
resetQueueCleanerConfig(): void {
|
||||
this.queueCleanerForm.reset({
|
||||
enabled: false,
|
||||
jobSchedule: {
|
||||
every: 5,
|
||||
type: ScheduleUnit.Minutes
|
||||
},
|
||||
runSequentially: false,
|
||||
ignoredDownloadsPath: '',
|
||||
|
||||
// Failed Import settings
|
||||
failedImportMaxStrikes: 0,
|
||||
failedImportIgnorePrivate: false,
|
||||
failedImportDeletePrivate: false,
|
||||
failedImportIgnorePatterns: [],
|
||||
|
||||
// Stalled settings
|
||||
stalledMaxStrikes: 0,
|
||||
stalledResetStrikesOnProgress: false,
|
||||
stalledIgnorePrivate: false,
|
||||
stalledDeletePrivate: false,
|
||||
|
||||
// Downloading Metadata settings
|
||||
downloadingMetadataMaxStrikes: 0,
|
||||
|
||||
// Slow Download settings
|
||||
slowMaxStrikes: 0,
|
||||
slowResetStrikesOnProgress: false,
|
||||
slowIgnorePrivate: false,
|
||||
slowDeletePrivate: false,
|
||||
slowMinSpeed: '',
|
||||
slowMaxTime: 0,
|
||||
slowIgnoreAboveSize: ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all controls in a form group as touched
|
||||
*/
|
||||
private markFormGroupTouched(formGroup: FormGroup): void {
|
||||
Object.values(formGroup.controls).forEach(control => {
|
||||
control.markAsTouched();
|
||||
|
||||
if ((control as any).controls) {
|
||||
this.markFormGroupTouched(control as FormGroup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a form control has an error after it's been touched
|
||||
*/
|
||||
hasError(controlName: string, errorName: string): boolean {
|
||||
const control = this.queueCleanerForm.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested form control errors
|
||||
*/
|
||||
hasNestedError(parentName: string, controlName: string, errorName: string): boolean {
|
||||
const parentControl = this.queueCleanerForm.get(parentName);
|
||||
if (!parentControl || !(parentControl instanceof FormGroup)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const control = parentControl.get(controlName);
|
||||
return control ? control.touched && control.hasError(errorName) : false;
|
||||
}
|
||||
}
|
||||
|
||||
117
code/UI/src/app/settings/store/queue-cleaner-config.store.ts
Normal file
117
code/UI/src/app/settings/store/queue-cleaner-config.store.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { patchState, signalStore, withHooks, withMethods, withState } from '@ngrx/signals';
|
||||
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { QueueCleanerConfig } from '../../shared/models/queue-cleaner-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap } from 'rxjs';
|
||||
import { MessageService } from 'primeng/api';
|
||||
|
||||
export interface QueueCleanerConfigState {
|
||||
config: QueueCleanerConfig | null;
|
||||
loading: boolean;
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: QueueCleanerConfigState = {
|
||||
config: null,
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class QueueCleanerConfigStore extends signalStore(
|
||||
withState(initialState),
|
||||
withMethods((store, configService = inject(ConfigurationService), messageService = inject(MessageService)) => ({
|
||||
|
||||
/**
|
||||
* Load the queue cleaner configuration
|
||||
*/
|
||||
loadConfig: rxMethod<void>(
|
||||
pipe => pipe.pipe(
|
||||
tap(() => patchState(store, { loading: true, error: null })),
|
||||
switchMap(() => configService.getQueueCleanerConfig().pipe(
|
||||
tap({
|
||||
next: (config) => patchState(store, { config, loading: false }),
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
loading: false,
|
||||
error: error.message || 'Failed to load configuration'
|
||||
});
|
||||
messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Load Error',
|
||||
detail: error.message || 'Failed to load queue cleaner configuration',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
/**
|
||||
* Save the queue cleaner configuration
|
||||
*/
|
||||
saveConfig: rxMethod<QueueCleanerConfig>(
|
||||
(config$: Observable<QueueCleanerConfig>) => config$.pipe(
|
||||
tap(() => patchState(store, { saving: true, error: null })),
|
||||
switchMap(config => configService.updateQueueCleanerConfig(config).pipe(
|
||||
tap({
|
||||
next: () => {
|
||||
patchState(store, {
|
||||
config,
|
||||
saving: false
|
||||
});
|
||||
messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: 'Queue cleaner configuration saved successfully',
|
||||
life: 3000
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
saving: false,
|
||||
error: error.message || 'Failed to save configuration'
|
||||
});
|
||||
messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Save Error',
|
||||
detail: error.message || 'Failed to save queue cleaner configuration',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
/**
|
||||
* Update config in the store without saving to the backend
|
||||
*/
|
||||
updateConfigLocally(config: Partial<QueueCleanerConfig>) {
|
||||
const currentConfig = store.config();
|
||||
if (currentConfig) {
|
||||
patchState(store, {
|
||||
config: { ...currentConfig, ...config }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset any errors
|
||||
*/
|
||||
resetError() {
|
||||
patchState(store, { error: null });
|
||||
}
|
||||
})),
|
||||
withHooks({
|
||||
onInit({ loadConfig }) {
|
||||
loadConfig();
|
||||
}
|
||||
})
|
||||
) {}
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="field-input byte-size-input">
|
||||
<div class="p-inputgroup">
|
||||
<p-inputNumber
|
||||
[ngModel]="value()"
|
||||
(ngModelChange)="value.set($event); updateValue()"
|
||||
[min]="min"
|
||||
[disabled]="disabled"
|
||||
[placeholder]="placeholder"
|
||||
[showButtons]="false"
|
||||
[inputStyleClass]="'w-full'"
|
||||
class="flex-auto">
|
||||
</p-inputNumber>
|
||||
|
||||
<p-selectButton
|
||||
[options]="unitOptions"
|
||||
[ngModel]="unit()"
|
||||
(ngModelChange)="unit.set($event); updateUnit()"
|
||||
[disabled]="disabled"
|
||||
optionLabel="label"
|
||||
optionValue="value">
|
||||
</p-selectButton>
|
||||
</div>
|
||||
|
||||
<small class="form-helper-text" *ngIf="helpText">{{ helpText }}</small>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
.byte-size-input {
|
||||
width: 100%;
|
||||
|
||||
.p-selectbutton {
|
||||
height: 100%;
|
||||
|
||||
.p-button {
|
||||
min-width: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-helper-text {
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Component, Input, forwardRef, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
||||
import { InputNumberModule } from 'primeng/inputnumber';
|
||||
import { SelectButtonModule } from 'primeng/selectbutton';
|
||||
|
||||
type ByteSizeUnit = 'KB' | 'MB' | 'GB' | 'TB';
|
||||
|
||||
@Component({
|
||||
selector: 'app-byte-size-input',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ReactiveFormsModule, InputNumberModule, SelectButtonModule],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => ByteSizeInputComponent),
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
templateUrl: './byte-size-input.component.html',
|
||||
styleUrl: './byte-size-input.component.scss'
|
||||
})
|
||||
export class ByteSizeInputComponent implements ControlValueAccessor {
|
||||
@Input() label: string = 'Size';
|
||||
@Input() min: number = 0;
|
||||
@Input() disabled: boolean = false;
|
||||
@Input() placeholder: string = 'Enter size';
|
||||
@Input() helpText: string = '';
|
||||
|
||||
// Value in the selected unit
|
||||
value = signal<number | null>(null);
|
||||
|
||||
// The selected unit
|
||||
unit = signal<ByteSizeUnit>('MB');
|
||||
|
||||
// Available units
|
||||
unitOptions = [
|
||||
{ label: 'KB', value: 'KB' },
|
||||
{ label: 'MB', value: 'MB' },
|
||||
{ label: 'GB', value: 'GB' },
|
||||
{ label: 'TB', value: 'TB' }
|
||||
];
|
||||
|
||||
// ControlValueAccessor interface methods
|
||||
private onChange: (value: string) => void = () => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
/**
|
||||
* Parse the string value in format '100MB', '1.5GB', etc.
|
||||
*/
|
||||
writeValue(value: string): void {
|
||||
if (!value) {
|
||||
this.value.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse values like "100MB", "1.5GB", etc.
|
||||
const regex = /^([\d.]+)([KMGT]B)$/i;
|
||||
const match = value.match(regex);
|
||||
|
||||
if (match) {
|
||||
const numValue = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase() as ByteSizeUnit;
|
||||
|
||||
this.value.set(numValue);
|
||||
this.unit.set(unit);
|
||||
} else {
|
||||
this.value.set(null);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing byte size value:', value, e);
|
||||
this.value.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the value and notify the form control
|
||||
*/
|
||||
updateValue(): void {
|
||||
this.onTouched();
|
||||
|
||||
if (this.value() === null) {
|
||||
this.onChange('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Format as "100MB", "1.5GB", etc.
|
||||
const formattedValue = `${this.value()}${this.unit()}`;
|
||||
this.onChange(formattedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the unit and notify the form control
|
||||
*/
|
||||
updateUnit(): void {
|
||||
this.updateValue();
|
||||
}
|
||||
}
|
||||
42
code/UI/src/app/shared/models/queue-cleaner-config.model.ts
Normal file
42
code/UI/src/app/shared/models/queue-cleaner-config.model.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export enum ScheduleUnit {
|
||||
Seconds = 'Seconds',
|
||||
Minutes = 'Minutes',
|
||||
Hours = 'Hours'
|
||||
}
|
||||
|
||||
export interface JobSchedule {
|
||||
every: number;
|
||||
type: ScheduleUnit;
|
||||
}
|
||||
|
||||
export interface QueueCleanerConfig {
|
||||
enabled: boolean;
|
||||
cronExpression: string;
|
||||
jobSchedule?: JobSchedule; // UI-only field, not sent to API
|
||||
runSequentially: boolean;
|
||||
ignoredDownloadsPath: string;
|
||||
|
||||
// Failed Import settings
|
||||
failedImportMaxStrikes: number;
|
||||
failedImportIgnorePrivate: boolean;
|
||||
failedImportDeletePrivate: boolean;
|
||||
failedImportIgnorePatterns: string[];
|
||||
|
||||
// Stalled settings
|
||||
stalledMaxStrikes: number;
|
||||
stalledResetStrikesOnProgress: boolean;
|
||||
stalledIgnorePrivate: boolean;
|
||||
stalledDeletePrivate: boolean;
|
||||
|
||||
// Downloading Metadata settings
|
||||
downloadingMetadataMaxStrikes: number;
|
||||
|
||||
// Slow Download settings
|
||||
slowMaxStrikes: number;
|
||||
slowResetStrikesOnProgress: boolean;
|
||||
slowIgnorePrivate: boolean;
|
||||
slowDeletePrivate: boolean;
|
||||
slowMinSpeed: string;
|
||||
slowMaxTime: number;
|
||||
slowIgnoreAboveSize: string;
|
||||
}
|
||||
Reference in New Issue
Block a user