mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-05-19 03:55:52 -04:00
Add test button for arrs and download clients (#391)
This commit is contained in:
@@ -177,7 +177,7 @@ public class StatusController : ControllerBase
|
||||
try
|
||||
{
|
||||
var sonarrClient = _arrClientFactory.GetClient(InstanceType.Sonarr);
|
||||
await sonarrClient.TestConnectionAsync(instance);
|
||||
await sonarrClient.HealthCheckAsync(instance);
|
||||
|
||||
sonarrStatus.Add(new
|
||||
{
|
||||
@@ -209,7 +209,7 @@ public class StatusController : ControllerBase
|
||||
try
|
||||
{
|
||||
var radarrClient = _arrClientFactory.GetClient(InstanceType.Radarr);
|
||||
await radarrClient.TestConnectionAsync(instance);
|
||||
await radarrClient.HealthCheckAsync(instance);
|
||||
|
||||
radarrStatus.Add(new
|
||||
{
|
||||
@@ -241,7 +241,7 @@ public class StatusController : ControllerBase
|
||||
try
|
||||
{
|
||||
var lidarrClient = _arrClientFactory.GetClient(InstanceType.Lidarr);
|
||||
await lidarrClient.TestConnectionAsync(instance);
|
||||
await lidarrClient.HealthCheckAsync(instance);
|
||||
|
||||
lidarrStatus.Add(new
|
||||
{
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
|
||||
namespace Cleanuparr.Api.Features.Arr.Contracts.Requests;
|
||||
|
||||
public sealed record TestArrInstanceRequest
|
||||
{
|
||||
[Required]
|
||||
public required string Url { get; init; }
|
||||
|
||||
[Required]
|
||||
public required string ApiKey { get; init; }
|
||||
|
||||
public ArrInstance ToTestInstance() => new()
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "Test Instance",
|
||||
Url = new Uri(Url),
|
||||
ApiKey = ApiKey,
|
||||
ArrConfigId = Guid.Empty,
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using Cleanuparr.Api.Features.Arr.Contracts.Requests;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Dtos;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Mapster;
|
||||
@@ -20,13 +21,16 @@ public sealed class ArrConfigController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<ArrConfigController> _logger;
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly IArrClientFactory _arrClientFactory;
|
||||
|
||||
public ArrConfigController(
|
||||
ILogger<ArrConfigController> logger,
|
||||
DataContext dataContext)
|
||||
DataContext dataContext,
|
||||
IArrClientFactory arrClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataContext = dataContext;
|
||||
_arrClientFactory = arrClientFactory;
|
||||
}
|
||||
|
||||
[HttpGet("sonarr")]
|
||||
@@ -124,6 +128,26 @@ public sealed class ArrConfigController : ControllerBase
|
||||
public Task<IActionResult> DeleteWhisparrInstance(Guid id)
|
||||
=> DeleteArrInstance(InstanceType.Whisparr, id);
|
||||
|
||||
[HttpPost("sonarr/instances/test")]
|
||||
public Task<IActionResult> TestSonarrInstance([FromBody] TestArrInstanceRequest request)
|
||||
=> TestArrInstance(InstanceType.Sonarr, request);
|
||||
|
||||
[HttpPost("radarr/instances/test")]
|
||||
public Task<IActionResult> TestRadarrInstance([FromBody] TestArrInstanceRequest request)
|
||||
=> TestArrInstance(InstanceType.Radarr, request);
|
||||
|
||||
[HttpPost("lidarr/instances/test")]
|
||||
public Task<IActionResult> TestLidarrInstance([FromBody] TestArrInstanceRequest request)
|
||||
=> TestArrInstance(InstanceType.Lidarr, request);
|
||||
|
||||
[HttpPost("readarr/instances/test")]
|
||||
public Task<IActionResult> TestReadarrInstance([FromBody] TestArrInstanceRequest request)
|
||||
=> TestArrInstance(InstanceType.Readarr, request);
|
||||
|
||||
[HttpPost("whisparr/instances/test")]
|
||||
public Task<IActionResult> TestWhisparrInstance([FromBody] TestArrInstanceRequest request)
|
||||
=> TestArrInstance(InstanceType.Whisparr, request);
|
||||
|
||||
private async Task<IActionResult> GetArrConfig(InstanceType type)
|
||||
{
|
||||
await DataContext.Lock.WaitAsync();
|
||||
@@ -260,6 +284,23 @@ public sealed class ArrConfigController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> TestArrInstance(InstanceType type, TestArrInstanceRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var testInstance = request.ToTestInstance();
|
||||
var client = _arrClientFactory.GetClient(type);
|
||||
await client.HealthCheckAsync(testInstance);
|
||||
|
||||
return Ok(new { Message = $"Connection to {type} instance successful" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test {Type} instance connection", type);
|
||||
return BadRequest(new { Message = $"Connection failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetConfigActionName(InstanceType type) => type switch
|
||||
{
|
||||
InstanceType.Sonarr => nameof(GetSonarrConfig),
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Exceptions;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
|
||||
namespace Cleanuparr.Api.Features.DownloadClient.Contracts.Requests;
|
||||
|
||||
public sealed record TestDownloadClientRequest
|
||||
{
|
||||
public DownloadClientTypeName TypeName { get; init; }
|
||||
|
||||
public DownloadClientType Type { get; init; }
|
||||
|
||||
public Uri? Host { get; init; }
|
||||
|
||||
public string? Username { get; init; }
|
||||
|
||||
public string? Password { get; init; }
|
||||
|
||||
public string? UrlBase { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (Host is null)
|
||||
{
|
||||
throw new ValidationException("Host cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadClientConfig ToTestConfig() => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Enabled = true,
|
||||
Name = "Test Client",
|
||||
TypeName = TypeName,
|
||||
Type = Type,
|
||||
Host = Host,
|
||||
Username = Username,
|
||||
Password = Password,
|
||||
UrlBase = UrlBase,
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Linq;
|
||||
|
||||
using Cleanuparr.Api.Features.DownloadClient.Contracts.Requests;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
@@ -18,15 +19,18 @@ public sealed class DownloadClientController : ControllerBase
|
||||
private readonly ILogger<DownloadClientController> _logger;
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly IDynamicHttpClientFactory _dynamicHttpClientFactory;
|
||||
private readonly IDownloadServiceFactory _downloadServiceFactory;
|
||||
|
||||
public DownloadClientController(
|
||||
ILogger<DownloadClientController> logger,
|
||||
DataContext dataContext,
|
||||
IDynamicHttpClientFactory dynamicHttpClientFactory)
|
||||
IDynamicHttpClientFactory dynamicHttpClientFactory,
|
||||
IDownloadServiceFactory downloadServiceFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataContext = dataContext;
|
||||
_dynamicHttpClientFactory = dynamicHttpClientFactory;
|
||||
_downloadServiceFactory = downloadServiceFactory;
|
||||
}
|
||||
|
||||
[HttpGet("download_client")]
|
||||
@@ -146,4 +150,33 @@ public sealed class DownloadClientController : ControllerBase
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("download_client/test")]
|
||||
public async Task<IActionResult> TestDownloadClient([FromBody] TestDownloadClientRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
request.Validate();
|
||||
|
||||
var testConfig = request.ToTestConfig();
|
||||
using var downloadService = _downloadServiceFactory.GetDownloadService(testConfig);
|
||||
var healthResult = await downloadService.HealthCheckAsync();
|
||||
|
||||
if (healthResult.IsHealthy)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
Message = $"Connection to {request.TypeName} successful",
|
||||
ResponseTime = healthResult.ResponseTime.TotalMilliseconds
|
||||
});
|
||||
}
|
||||
|
||||
return BadRequest(new { Message = healthResult.ErrorMessage ?? "Connection failed" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test {TypeName} client connection", request.TypeName);
|
||||
return BadRequest(new { Message = $"Connection failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,12 +586,12 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
};
|
||||
|
||||
await _notificationService.SendTestNotificationAsync(providerDto);
|
||||
return Ok(new { Message = "Test notification sent successfully", Success = true });
|
||||
return Ok(new { Message = "Test notification sent successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test Notifiarr provider");
|
||||
throw;
|
||||
return BadRequest(new { Message = $"Test failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,12 +627,12 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
};
|
||||
|
||||
await _notificationService.SendTestNotificationAsync(providerDto);
|
||||
return Ok(new { Message = "Test notification sent successfully", Success = true });
|
||||
return Ok(new { Message = "Test notification sent successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test Apprise provider");
|
||||
throw;
|
||||
return BadRequest(new { Message = $"Test failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,12 +673,12 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
};
|
||||
|
||||
await _notificationService.SendTestNotificationAsync(providerDto);
|
||||
return Ok(new { Message = "Test notification sent successfully", Success = true });
|
||||
return Ok(new { Message = "Test notification sent successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test Ntfy provider");
|
||||
throw;
|
||||
return BadRequest(new { Message = $"Test failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -899,12 +899,12 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
};
|
||||
|
||||
await _notificationService.SendTestNotificationAsync(providerDto);
|
||||
return Ok(new { Message = "Test notification sent successfully", Success = true });
|
||||
return Ok(new { Message = "Test notification sent successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test Pushover provider");
|
||||
throw;
|
||||
return BadRequest(new { Message = $"Test failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,16 +168,12 @@ public abstract class ArrClient : IArrClient
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the connection to an Arr instance
|
||||
/// </summary>
|
||||
/// <param name="arrInstance">The instance to test connection to</param>
|
||||
/// <returns>Task that completes when the connection test is done</returns>
|
||||
public virtual async Task TestConnectionAsync(ArrInstance arrInstance)
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HealthCheckAsync(ArrInstance arrInstance)
|
||||
{
|
||||
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/system/status";
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}{GetSystemStatusUrlPath()}";
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
@@ -188,6 +184,8 @@ public abstract class ArrClient : IArrClient
|
||||
|
||||
_logger.LogDebug("Connection test successful for {url}", arrInstance.Url);
|
||||
}
|
||||
|
||||
protected abstract string GetSystemStatusUrlPath();
|
||||
|
||||
protected abstract string GetQueueUrlPath();
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ public interface IArrClient
|
||||
Task SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
|
||||
|
||||
bool IsRecordValid(QueueRecord record);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tests the connection to an Arr instance
|
||||
/// </summary>
|
||||
/// <param name="arrInstance">The instance to test connection to</param>
|
||||
/// <returns>Task that completes when the connection test is done</returns>
|
||||
Task TestConnectionAsync(ArrInstance arrInstance);
|
||||
Task HealthCheckAsync(ArrInstance arrInstance);
|
||||
}
|
||||
@@ -22,6 +22,11 @@ public class LidarrClient : ArrClient, ILidarrClient
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v1/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
return "/api/v1/queue";
|
||||
|
||||
@@ -21,6 +21,11 @@ public class RadarrClient : ArrClient, IRadarrClient
|
||||
) : base(logger, httpClientFactory, striker, dryRunInterceptor)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v3/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
|
||||
@@ -21,6 +21,11 @@ public class ReadarrClient : ArrClient, IReadarrClient
|
||||
) : base(logger, httpClientFactory, striker, dryRunInterceptor)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v1/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
|
||||
@@ -25,6 +25,11 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v3/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
return "/api/v3/queue";
|
||||
|
||||
@@ -25,6 +25,11 @@ public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v3/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
return "/api/v3/queue";
|
||||
|
||||
@@ -8,8 +8,8 @@ import { RadarrConfig } from "../../shared/models/radarr-config.model";
|
||||
import { LidarrConfig } from "../../shared/models/lidarr-config.model";
|
||||
import { ReadarrConfig } from "../../shared/models/readarr-config.model";
|
||||
import { WhisparrConfig } from "../../shared/models/whisparr-config.model";
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto } from "../../shared/models/download-client-config.model";
|
||||
import { ArrInstance, CreateArrInstanceDto } from "../../shared/models/arr-config.model";
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto, TestDownloadClientRequest, TestConnectionResult } from "../../shared/models/download-client-config.model";
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
import { GeneralConfig } from "../../shared/models/general-config.model";
|
||||
import {
|
||||
StallRule,
|
||||
@@ -765,4 +765,84 @@ export class ConfigurationService {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// ===== CONNECTION TESTING =====
|
||||
|
||||
/**
|
||||
* Test a Sonarr instance connection
|
||||
*/
|
||||
testSonarrInstance(request: TestArrInstanceRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/sonarr/instances/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing Sonarr instance:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a Radarr instance connection
|
||||
*/
|
||||
testRadarrInstance(request: TestArrInstanceRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/radarr/instances/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing Radarr instance:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a Lidarr instance connection
|
||||
*/
|
||||
testLidarrInstance(request: TestArrInstanceRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/lidarr/instances/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing Lidarr instance:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a Readarr instance connection
|
||||
*/
|
||||
testReadarrInstance(request: TestArrInstanceRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/readarr/instances/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing Readarr instance:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a Whisparr instance connection
|
||||
*/
|
||||
testWhisparrInstance(request: TestArrInstanceRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/whisparr/instances/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing Whisparr instance:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a download client connection
|
||||
*/
|
||||
testDownloadClient(request: TestDownloadClientRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/download_client/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing download client:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { patchState, signalStore, withHooks, withMethods, withState } from '@ngrx/signals';
|
||||
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto } from '../../shared/models/download-client-config.model';
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto, TestDownloadClientRequest, TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
|
||||
@@ -11,6 +11,10 @@ export interface DownloadClientConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
pendingOperations: number;
|
||||
testing: boolean;
|
||||
testingClientId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: DownloadClientConfigState = {
|
||||
@@ -18,7 +22,11 @@ const initialState: DownloadClientConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
pendingOperations: 0
|
||||
pendingOperations: 0,
|
||||
testing: false,
|
||||
testingClientId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -91,7 +99,41 @@ export class DownloadClientConfigStore extends signalStore(
|
||||
resetError() {
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingClientId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a download client connection
|
||||
*/
|
||||
testClient: rxMethod<{ request: TestDownloadClientRequest; clientId?: string }>(
|
||||
(params$: Observable<{ request: TestDownloadClientRequest; clientId?: string }>) => params$.pipe(
|
||||
tap(({ clientId }) => patchState(store, { testing: true, testingClientId: clientId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testDownloadClient(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
/**
|
||||
* Batch create multiple clients
|
||||
*/
|
||||
|
||||
@@ -68,20 +68,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || downloadClientSaving()"
|
||||
[loading]="testingClientId() === client.id"
|
||||
(click)="testClientFromList(client)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit client"
|
||||
tooltipPosition="left"
|
||||
[disabled]="downloadClientSaving()"
|
||||
(click)="openEditClientModal(client)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete client"
|
||||
tooltipPosition="left"
|
||||
@@ -248,17 +259,27 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeClientModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="clientForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testClientFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="clientForm.invalid || downloadClientSaving()"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, EventEmitter, OnDestroy, Output, effect, inject } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from "@angular/forms";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { DownloadClientConfigStore } from "./download-client-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto } from "../../shared/models/download-client-config.model";
|
||||
import { ClientConfig, CreateDownloadClientDto, TestDownloadClientRequest } from "../../shared/models/download-client-config.model";
|
||||
import { DownloadClientType, DownloadClientTypeName } from "../../shared/models/enums";
|
||||
import { DocumentationService } from "../../core/services/documentation.service";
|
||||
|
||||
@@ -76,6 +76,10 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
|
||||
downloadClientLoading = this.downloadClientStore.loading;
|
||||
downloadClientError = this.downloadClientStore.error;
|
||||
downloadClientSaving = this.downloadClientStore.saving;
|
||||
testing = this.downloadClientStore.testing;
|
||||
testingClientId = this.downloadClientStore.testingClientId;
|
||||
testError = this.downloadClientStore.testError;
|
||||
testResult = this.downloadClientStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -113,6 +117,22 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
|
||||
.subscribe(() => {
|
||||
this.onClientTypeChange();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.downloadClientStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.downloadClientStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,4 +392,43 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
|
||||
openFieldDocs(fieldName: string): void {
|
||||
this.documentationService.openFieldDocumentation('download-client', fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test client connection from the modal (new or editing)
|
||||
*/
|
||||
testClientFromModal(): void {
|
||||
if (this.clientForm.invalid) {
|
||||
this.markFormGroupTouched(this.clientForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const formValue = this.clientForm.value;
|
||||
const testRequest: TestDownloadClientRequest = {
|
||||
typeName: formValue.typeName,
|
||||
type: this.mapTypeNameToType(formValue.typeName),
|
||||
host: formValue.host,
|
||||
username: formValue.username,
|
||||
password: formValue.password,
|
||||
urlBase: formValue.urlBase,
|
||||
};
|
||||
|
||||
this.downloadClientStore.testClient({ request: testRequest, clientId: this.editingClient?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test client connection from the list view (existing client)
|
||||
*/
|
||||
testClientFromList(client: ClientConfig): void {
|
||||
const testRequest: TestDownloadClientRequest = {
|
||||
typeName: client.typeName,
|
||||
type: client.type,
|
||||
host: client.host,
|
||||
username: client.username,
|
||||
password: client.password,
|
||||
urlBase: client.urlBase,
|
||||
};
|
||||
|
||||
this.downloadClientStore.testClient({ request: testRequest, clientId: client.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { LidarrConfig } from '../../shared/models/lidarr-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
import { ArrInstance, CreateArrInstanceDto } from '../../shared/models/arr-config.model';
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from '../../shared/models/arr-config.model';
|
||||
import { TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
|
||||
export interface LidarrConfigState {
|
||||
config: LidarrConfig | null;
|
||||
@@ -12,6 +13,10 @@ export interface LidarrConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
instanceOperations: number;
|
||||
testing: boolean;
|
||||
testingInstanceId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: LidarrConfigState = {
|
||||
@@ -19,7 +24,11 @@ const initialState: LidarrConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
instanceOperations: 0
|
||||
instanceOperations: 0,
|
||||
testing: false,
|
||||
testingInstanceId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -123,6 +132,40 @@ export class LidarrConfigStore extends signalStore(
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingInstanceId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a Lidarr instance connection
|
||||
*/
|
||||
testInstance: rxMethod<{ request: TestArrInstanceRequest; instanceId?: string }>(
|
||||
(params$: Observable<{ request: TestArrInstanceRequest; instanceId?: string }>) => params$.pipe(
|
||||
tap(({ instanceId }) => patchState(store, { testing: true, testingInstanceId: instanceId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testLidarrInstance(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
// ===== INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,20 +111,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || lidarrSaving()"
|
||||
[loading]="testingInstanceId() === instance.id"
|
||||
(click)="testInstanceFromList(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit instance"
|
||||
tooltipPosition="left"
|
||||
[disabled]="lidarrSaving()"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete instance"
|
||||
tooltipPosition="left"
|
||||
@@ -216,19 +227,29 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="instanceForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testInstanceFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="instanceForm.invalid || lidarrSaving()"
|
||||
[loading]="lidarrSaving()"
|
||||
(click)="saveInstance()"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { LidarrConfigStore } from "./lidarr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { LidarrConfig } from "../../shared/models/lidarr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance } from "../../shared/models/arr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -74,6 +74,10 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
lidarrLoading = this.lidarrStore.loading;
|
||||
lidarrError = this.lidarrStore.error;
|
||||
lidarrSaving = this.lidarrStore.saving;
|
||||
testing = this.lidarrStore.testing;
|
||||
testingInstanceId = this.lidarrStore.testingInstanceId;
|
||||
testError = this.lidarrStore.testError;
|
||||
testResult = this.lidarrStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -112,6 +116,22 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
.subscribe(() => {
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.lidarrStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.lidarrStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,4 +438,34 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
get modalTitle(): string {
|
||||
return this.modalMode === 'add' ? 'Add Lidarr Instance' : 'Edit Lidarr Instance';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the modal (new or editing)
|
||||
*/
|
||||
testInstanceFromModal(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
this.lidarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the list view (existing instance)
|
||||
*/
|
||||
testInstanceFromList(instance: ArrInstance): void {
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
};
|
||||
|
||||
this.lidarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,25 +211,17 @@ export class NotificationProviderConfigStore extends signalStore(
|
||||
},
|
||||
error: (error) => {
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: errorMessage, // Test errors should NOT trigger "Not connected" state
|
||||
testResult: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: errorMessage // Test errors should NOT trigger "Not connected" state
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError((error) => {
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: errorMessage, // Test errors should NOT trigger "Not connected" state
|
||||
testResult: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: errorMessage // Test errors should NOT trigger "Not connected" state
|
||||
});
|
||||
return EMPTY;
|
||||
})
|
||||
|
||||
@@ -130,16 +130,11 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
}
|
||||
});
|
||||
|
||||
// Setup effect to react to test results
|
||||
// Setup effect to react to test results (HTTP 200 = success)
|
||||
effect(() => {
|
||||
const result = this.testResult();
|
||||
if (result) {
|
||||
if (result.success) {
|
||||
this.notificationService.showSuccess(result.message || "Test notification sent successfully");
|
||||
} else {
|
||||
// Error handling is already done in the test error effect above
|
||||
// This just handles the success case
|
||||
}
|
||||
this.notificationService.showSuccess(result.message || "Test notification sent successfully");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { RadarrConfig } from '../../shared/models/radarr-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
import { ArrInstance, CreateArrInstanceDto } from '../../shared/models/arr-config.model';
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from '../../shared/models/arr-config.model';
|
||||
import { TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
|
||||
export interface RadarrConfigState {
|
||||
config: RadarrConfig | null;
|
||||
@@ -12,6 +13,10 @@ export interface RadarrConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
instanceOperations: number;
|
||||
testing: boolean;
|
||||
testingInstanceId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: RadarrConfigState = {
|
||||
@@ -19,7 +24,11 @@ const initialState: RadarrConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
instanceOperations: 0
|
||||
instanceOperations: 0,
|
||||
testing: false,
|
||||
testingInstanceId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -123,6 +132,40 @@ export class RadarrConfigStore extends signalStore(
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingInstanceId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a Radarr instance connection
|
||||
*/
|
||||
testInstance: rxMethod<{ request: TestArrInstanceRequest; instanceId?: string }>(
|
||||
(params$: Observable<{ request: TestArrInstanceRequest; instanceId?: string }>) => params$.pipe(
|
||||
tap(({ instanceId }) => patchState(store, { testing: true, testingInstanceId: instanceId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testRadarrInstance(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
// ===== INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,20 +111,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || radarrSaving()"
|
||||
[loading]="testingInstanceId() === instance.id"
|
||||
(click)="testInstanceFromList(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit instance"
|
||||
tooltipPosition="left"
|
||||
[disabled]="radarrSaving()"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete instance"
|
||||
tooltipPosition="left"
|
||||
@@ -216,17 +227,27 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="instanceForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testInstanceFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="instanceForm.invalid || radarrSaving()"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { RadarrConfigStore } from "./radarr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { RadarrConfig } from "../../shared/models/radarr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance } from "../../shared/models/arr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -74,6 +74,10 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
radarrLoading = this.radarrStore.loading;
|
||||
radarrError = this.radarrStore.error;
|
||||
radarrSaving = this.radarrStore.saving;
|
||||
testing = this.radarrStore.testing;
|
||||
testingInstanceId = this.radarrStore.testingInstanceId;
|
||||
testError = this.radarrStore.testError;
|
||||
testResult = this.radarrStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -112,6 +116,22 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
.subscribe(() => {
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.radarrStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.radarrStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,4 +438,34 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
get modalTitle(): string {
|
||||
return this.modalMode === 'add' ? 'Add Radarr Instance' : 'Edit Radarr Instance';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the modal (new or editing)
|
||||
*/
|
||||
testInstanceFromModal(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
this.radarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the list view (existing instance)
|
||||
*/
|
||||
testInstanceFromList(instance: ArrInstance): void {
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
};
|
||||
|
||||
this.radarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { ReadarrConfig } from '../../shared/models/readarr-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
import { ArrInstance, CreateArrInstanceDto } from '../../shared/models/arr-config.model';
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from '../../shared/models/arr-config.model';
|
||||
import { TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
|
||||
export interface ReadarrConfigState {
|
||||
config: ReadarrConfig | null;
|
||||
@@ -12,6 +13,10 @@ export interface ReadarrConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
instanceOperations: number;
|
||||
testing: boolean;
|
||||
testingInstanceId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: ReadarrConfigState = {
|
||||
@@ -19,7 +24,11 @@ const initialState: ReadarrConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
instanceOperations: 0
|
||||
instanceOperations: 0,
|
||||
testing: false,
|
||||
testingInstanceId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -123,6 +132,40 @@ export class ReadarrConfigStore extends signalStore(
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingInstanceId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a Readarr instance connection
|
||||
*/
|
||||
testInstance: rxMethod<{ request: TestArrInstanceRequest; instanceId?: string }>(
|
||||
(params$: Observable<{ request: TestArrInstanceRequest; instanceId?: string }>) => params$.pipe(
|
||||
tap(({ instanceId }) => patchState(store, { testing: true, testingInstanceId: instanceId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testReadarrInstance(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
// ===== INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,20 +111,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || readarrSaving()"
|
||||
[loading]="testingInstanceId() === instance.id"
|
||||
(click)="testInstanceFromList(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit instance"
|
||||
tooltipPosition="left"
|
||||
[disabled]="readarrSaving()"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete instance"
|
||||
tooltipPosition="left"
|
||||
@@ -216,17 +227,27 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="instanceForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testInstanceFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="instanceForm.invalid || readarrSaving()"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { ReadarrConfigStore } from "./readarr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { ReadarrConfig } from "../../shared/models/readarr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance } from "../../shared/models/arr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -74,6 +74,10 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
readarrLoading = this.readarrStore.loading;
|
||||
readarrError = this.readarrStore.error;
|
||||
readarrSaving = this.readarrStore.saving;
|
||||
testing = this.readarrStore.testing;
|
||||
testingInstanceId = this.readarrStore.testingInstanceId;
|
||||
testError = this.readarrStore.testError;
|
||||
testResult = this.readarrStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -112,6 +116,22 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
.subscribe(() => {
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.readarrStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.readarrStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,4 +438,34 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
const control = parentControl.get(controlName);
|
||||
return control ? control.dirty && control.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the modal (new or editing)
|
||||
*/
|
||||
testInstanceFromModal(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
this.readarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the list view (existing instance)
|
||||
*/
|
||||
testInstanceFromList(instance: ArrInstance): void {
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
};
|
||||
|
||||
this.readarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { SonarrConfig } from '../../shared/models/sonarr-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
import { ArrInstance, CreateArrInstanceDto } from '../../shared/models/arr-config.model';
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from '../../shared/models/arr-config.model';
|
||||
import { TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
|
||||
export interface SonarrConfigState {
|
||||
config: SonarrConfig | null;
|
||||
@@ -12,6 +13,10 @@ export interface SonarrConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
instanceOperations: number;
|
||||
testing: boolean;
|
||||
testingInstanceId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: SonarrConfigState = {
|
||||
@@ -19,7 +24,11 @@ const initialState: SonarrConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
instanceOperations: 0
|
||||
instanceOperations: 0,
|
||||
testing: false,
|
||||
testingInstanceId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -97,6 +106,40 @@ export class SonarrConfigStore extends signalStore(
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingInstanceId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a Sonarr instance connection
|
||||
*/
|
||||
testInstance: rxMethod<{ request: TestArrInstanceRequest; instanceId?: string }>(
|
||||
(params$: Observable<{ request: TestArrInstanceRequest; instanceId?: string }>) => params$.pipe(
|
||||
tap(({ instanceId }) => patchState(store, { testing: true, testingInstanceId: instanceId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testSonarrInstance(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
// ===== INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,20 +111,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || sonarrSaving()"
|
||||
[loading]="testingInstanceId() === instance.id"
|
||||
(click)="testInstanceFromList(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit instance"
|
||||
tooltipPosition="left"
|
||||
[disabled]="sonarrSaving()"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete instance"
|
||||
tooltipPosition="left"
|
||||
@@ -216,17 +227,27 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="instanceForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testInstanceFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="instanceForm.invalid || sonarrSaving()"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { SonarrConfigStore } from "./sonarr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { SonarrConfig } from "../../shared/models/sonarr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance } from "../../shared/models/arr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -74,6 +74,10 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
sonarrLoading = this.sonarrStore.loading;
|
||||
sonarrError = this.sonarrStore.error;
|
||||
sonarrSaving = this.sonarrStore.saving;
|
||||
testing = this.sonarrStore.testing;
|
||||
testingInstanceId = this.sonarrStore.testingInstanceId;
|
||||
testError = this.sonarrStore.testError;
|
||||
testResult = this.sonarrStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -112,6 +116,22 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
.subscribe(() => {
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.sonarrStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.sonarrStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,4 +438,34 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
get modalTitle(): string {
|
||||
return this.modalMode === 'add' ? 'Add Sonarr Instance' : 'Edit Sonarr Instance';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the modal (new or editing)
|
||||
*/
|
||||
testInstanceFromModal(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
this.sonarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the list view (existing instance)
|
||||
*/
|
||||
testInstanceFromList(instance: ArrInstance): void {
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
};
|
||||
|
||||
this.sonarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { WhisparrConfig } from '../../shared/models/whisparr-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
import { ArrInstance, CreateArrInstanceDto } from '../../shared/models/arr-config.model';
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from '../../shared/models/arr-config.model';
|
||||
import { TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
|
||||
export interface WhisparrConfigState {
|
||||
config: WhisparrConfig | null;
|
||||
@@ -12,6 +13,10 @@ export interface WhisparrConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
instanceOperations: number;
|
||||
testing: boolean;
|
||||
testingInstanceId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: WhisparrConfigState = {
|
||||
@@ -19,7 +24,11 @@ const initialState: WhisparrConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
instanceOperations: 0
|
||||
instanceOperations: 0,
|
||||
testing: false,
|
||||
testingInstanceId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -123,6 +132,40 @@ export class WhisparrConfigStore extends signalStore(
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingInstanceId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a Whisparr instance connection
|
||||
*/
|
||||
testInstance: rxMethod<{ request: TestArrInstanceRequest; instanceId?: string }>(
|
||||
(params$: Observable<{ request: TestArrInstanceRequest; instanceId?: string }>) => params$.pipe(
|
||||
tap(({ instanceId }) => patchState(store, { testing: true, testingInstanceId: instanceId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testWhisparrInstance(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
// ===== INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,20 +111,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || whisparrSaving()"
|
||||
[loading]="testingInstanceId() === instance.id"
|
||||
(click)="testInstanceFromList(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit instance"
|
||||
tooltipPosition="left"
|
||||
[disabled]="whisparrSaving()"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete instance"
|
||||
tooltipPosition="left"
|
||||
@@ -216,17 +227,27 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="instanceForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testInstanceFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="instanceForm.invalid || whisparrSaving()"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { WhisparrConfigStore } from "./whisparr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { WhisparrConfig } from "../../shared/models/whisparr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance } from "../../shared/models/arr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -74,6 +74,10 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
whisparrLoading = this.whisparrStore.loading;
|
||||
whisparrError = this.whisparrStore.error;
|
||||
whisparrSaving = this.whisparrStore.saving;
|
||||
testing = this.whisparrStore.testing;
|
||||
testingInstanceId = this.whisparrStore.testingInstanceId;
|
||||
testError = this.whisparrStore.testError;
|
||||
testResult = this.whisparrStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -112,6 +116,22 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
.subscribe(() => {
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.whisparrStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.whisparrStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -413,4 +433,34 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
get modalTitle(): string {
|
||||
return this.modalMode === 'add' ? 'Add Whisparr Instance' : 'Edit Whisparr Instance';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the modal (new or editing)
|
||||
*/
|
||||
testInstanceFromModal(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
this.whisparrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the list view (existing instance)
|
||||
*/
|
||||
testInstanceFromList(instance: ArrInstance): void {
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
};
|
||||
|
||||
this.whisparrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
}
|
||||
}
|
||||
@@ -18,3 +18,11 @@ export interface CreateArrInstanceDto {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for testing an Arr instance connection
|
||||
*/
|
||||
export interface TestArrInstanceRequest {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
@@ -124,3 +124,23 @@ export interface ClientConfigUpdateDto extends ClientConfig {
|
||||
*/
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for testing a download client connection
|
||||
*/
|
||||
export interface TestDownloadClientRequest {
|
||||
typeName: DownloadClientTypeName;
|
||||
type: DownloadClientType;
|
||||
host?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
urlBase?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of testing a connection (HTTP 200 = success)
|
||||
*/
|
||||
export interface TestConnectionResult {
|
||||
message: string;
|
||||
responseTime?: number;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,5 @@ export interface AppriseConfiguration {
|
||||
}
|
||||
|
||||
export interface TestNotificationResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user