feat: Notify users about invalid and incomplete settings
This commit is contained in:
@@ -33,8 +33,6 @@ function getRadarrApi() {
|
||||
|
||||
if (!baseUrl || !apiKey || !rootFolder || !qualityProfileId) return undefined;
|
||||
|
||||
console.log(baseUrl, apiKey);
|
||||
|
||||
return createClient<paths>({
|
||||
baseUrl,
|
||||
headers: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { notificationStack } from '$lib/stores/notification.store';
|
||||
import classNames from 'classnames';
|
||||
import { Cross2 } from 'radix-icons-svelte';
|
||||
import { Cross2, ExclamationTriangle } from 'radix-icons-svelte';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import IconButton from '../IconButton.svelte';
|
||||
|
||||
@@ -11,15 +11,23 @@
|
||||
export let description: string;
|
||||
export let duration = 0;
|
||||
|
||||
export let type: 'info' | 'error' | 'warning' = 'info';
|
||||
|
||||
function handleClose() {
|
||||
console.log('close');
|
||||
notificationStack.close(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="bg-zinc-900 bg-opacity-60 rounded-lg backdrop-blur-xl overflow-hidden
|
||||
flex flex-col w-72"
|
||||
class={classNames(
|
||||
'bg-opacity-60 rounded-lg backdrop-blur-xl overflow-hidden',
|
||||
'flex flex-col w-72',
|
||||
{
|
||||
'bg-zinc-900': type === 'info',
|
||||
'bg-red-900': type === 'error',
|
||||
'bg-yellow-900': type === 'warning'
|
||||
}
|
||||
)}
|
||||
in:fly|global={{ duration: 150, x: 50 }}
|
||||
out:fade|global={{ duration: 150 }}
|
||||
>
|
||||
@@ -34,7 +42,10 @@ flex flex-col w-72"
|
||||
<div
|
||||
class="relative z-[1] flex items-center justify-between bg-zinc-200 bg-opacity-10 p-1 px-3"
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if type !== 'info'}
|
||||
<ExclamationTriangle size={12} />
|
||||
{/if}
|
||||
<h1 class="text-zinc-200 font-medium text-sm">{title}</h1>
|
||||
</div>
|
||||
<IconButton on:click={handleClose}>
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatcher = createEventDispatcher();
|
||||
|
||||
export let type: 'text' | 'number' = 'text';
|
||||
export let value: any = type === 'text' ? '' : 0;
|
||||
export let placeholder = '';
|
||||
|
||||
function handleChange(event: Event) {
|
||||
value = (event.target as HTMLInputElement).value;
|
||||
dispatcher('change', value);
|
||||
}
|
||||
|
||||
const baseStyles =
|
||||
'appearance-none p-1 px-3 selectable border border-zinc-800 rounded-lg bg-zinc-600 bg-opacity-20 text-zinc-200 placeholder:text-zinc-700';
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
{#if type === 'text'}
|
||||
<input type="text" {placeholder} bind:value class={classNames(baseStyles, $$restProps.class)} />
|
||||
<input
|
||||
type="text"
|
||||
{placeholder}
|
||||
bind:value
|
||||
on:input={handleChange}
|
||||
class={classNames(baseStyles, $$restProps.class)}
|
||||
/>
|
||||
{:else if type === 'number'}
|
||||
<input
|
||||
type="number"
|
||||
{placeholder}
|
||||
bind:value
|
||||
on:input={handleChange}
|
||||
class={classNames(baseStyles, 'w-28', $$restProps.class)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'reflect-metadata';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Settings } from './entities/Settings';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
class TypeOrm {
|
||||
private static instance: Promise<DataSource | null> | null = null;
|
||||
@@ -14,7 +15,7 @@ class TypeOrm {
|
||||
TypeOrm.instance = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: 'config/reiverr.sqlite',
|
||||
synchronize: true,
|
||||
synchronize: dev,
|
||||
entities: [Settings],
|
||||
logging: true
|
||||
})
|
||||
|
||||
@@ -85,11 +85,12 @@
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected"
|
||||
},
|
||||
"options:": {
|
||||
"options": {
|
||||
"options": "Options",
|
||||
"rootFolder": "Root Folder",
|
||||
"qualityProfile": "Quality Profile",
|
||||
"languageProfile": "Language Profile"
|
||||
"languageProfile": "Language Profile",
|
||||
"jellyfinUser": "Jellyfin User"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import Notification from '$lib/components/Notification/Notification.svelte';
|
||||
|
||||
export type NotificationItem = {
|
||||
id: symbol;
|
||||
@@ -57,3 +58,11 @@ function createNotificationStack() {
|
||||
}
|
||||
|
||||
export const notificationStack = createNotificationStack();
|
||||
|
||||
export function createErrorNotification(title: string, details: string, type = 'error') {
|
||||
return notificationStack.create(Notification, {
|
||||
type,
|
||||
title,
|
||||
description: details
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import IntegrationSettingsPage from './IntegrationSettingsPage.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { createErrorNotification } from '$lib/stores/notification.store';
|
||||
|
||||
type Section = 'general' | 'integrations';
|
||||
|
||||
@@ -25,12 +26,16 @@
|
||||
|
||||
let values: SettingsValues;
|
||||
let initialValues: SettingsValues;
|
||||
settings.subscribe((v) => {
|
||||
settings.subscribe(async (v) => {
|
||||
values = structuredClone(v);
|
||||
initialValues = structuredClone(v);
|
||||
if (values.sonarr.baseUrl && values.sonarr.apiKey) checkSonarrHealth();
|
||||
if (values.radarr.baseUrl && values.radarr.apiKey) checkRadarrHealth();
|
||||
if (values.jellyfin.baseUrl && values.jellyfin.apiKey) checkJellyfinHealth();
|
||||
const s = updateSonarrHealth();
|
||||
const r = updateRadarrHealth();
|
||||
const j = updateJellyfinHealth();
|
||||
|
||||
await Promise.all([s, r, j]);
|
||||
|
||||
checkForPartialConfiguration(v);
|
||||
});
|
||||
|
||||
let valuesChanged = false;
|
||||
@@ -49,7 +54,11 @@
|
||||
values.sonarr.baseUrl &&
|
||||
!(await getSonarrHealth(values.sonarr.baseUrl, values.sonarr.apiKey))
|
||||
) {
|
||||
throw new Error('Could not connect to Sonarr');
|
||||
createErrorNotification(
|
||||
'Invalid Configuration',
|
||||
'Could not connect to Sonarr. Check Sonarr credentials.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -57,27 +66,58 @@
|
||||
values.radarr.baseUrl &&
|
||||
!(await getRadarrHealth(values.radarr.baseUrl, values.radarr.apiKey))
|
||||
) {
|
||||
throw new Error('Could not connect to Radarr');
|
||||
createErrorNotification(
|
||||
'Invalid Configuration',
|
||||
'Could not connect to Radarr. Check Radarr credentials.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.jellyfin.apiKey && values.jellyfin.baseUrl) {
|
||||
if (!(await getJellyfinHealth(values.jellyfin.baseUrl, values.jellyfin.apiKey)))
|
||||
throw new Error('Could not connect to Jellyfin');
|
||||
if (!(await getJellyfinHealth(values.jellyfin.baseUrl, values.jellyfin.apiKey))) {
|
||||
createErrorNotification(
|
||||
'Invalid Configuration',
|
||||
'Could not connect to Jellyfin. Check Jellyfin credentials.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const users = await getJellyfinUsers(values.jellyfin.baseUrl, values.jellyfin.apiKey);
|
||||
if (!users.find((u) => u.Id === values.jellyfin.userId)) values.jellyfin.userId = null;
|
||||
}
|
||||
|
||||
checkSonarrHealth();
|
||||
checkRadarrHealth();
|
||||
checkJellyfinHealth();
|
||||
updateSonarrHealth();
|
||||
updateRadarrHealth();
|
||||
updateJellyfinHealth();
|
||||
|
||||
axios.post('/api/settings', values).then(() => {
|
||||
settings.set(values);
|
||||
});
|
||||
}
|
||||
|
||||
async function checkSonarrHealth(): Promise<boolean> {
|
||||
if (!values.sonarr.baseUrl || !values.sonarr.apiKey) {
|
||||
function checkForPartialConfiguration(v: SettingsValues) {
|
||||
let error = '';
|
||||
if (sonarrConnected && !v.sonarr.rootFolderPath) {
|
||||
error = 'Sonarr disabled: Root folder path is required';
|
||||
} else if (sonarrConnected && !v.sonarr.qualityProfileId) {
|
||||
error = 'Sonarr disabled: Quality profile is required';
|
||||
} else if (sonarrConnected && !v.sonarr.languageProfileId) {
|
||||
error = 'Sonarr disabled: Language profile is required';
|
||||
}
|
||||
|
||||
if (radarrConnected && !v.radarr.rootFolderPath) {
|
||||
error = 'Radarr disabled: Root folder path is required';
|
||||
} else if (radarrConnected && !v.radarr.qualityProfileId) {
|
||||
error = 'Radarr disabled: Quality profile is required';
|
||||
}
|
||||
|
||||
if (jellyfinConnected && !v.jellyfin.userId) {
|
||||
error = 'Jellyfin disabled: User is required';
|
||||
}
|
||||
if (error) createErrorNotification('Incomplete Configuration', error, 'warning');
|
||||
}
|
||||
|
||||
async function updateSonarrHealth(reset = false): Promise<boolean> {
|
||||
if (!values.sonarr.baseUrl || !values.sonarr.apiKey || reset) {
|
||||
sonarrConnected = false;
|
||||
return false;
|
||||
}
|
||||
@@ -90,8 +130,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function checkRadarrHealth(): Promise<boolean> {
|
||||
if (!values.radarr.baseUrl || !values.radarr.apiKey) {
|
||||
async function updateRadarrHealth(reset = false): Promise<boolean> {
|
||||
if (!values.radarr.baseUrl || !values.radarr.apiKey || reset) {
|
||||
radarrConnected = false;
|
||||
return false;
|
||||
}
|
||||
@@ -104,11 +144,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function checkJellyfinHealth(): Promise<boolean> {
|
||||
if (!values.jellyfin.baseUrl || !values.jellyfin.apiKey) {
|
||||
async function updateJellyfinHealth(reset = false): Promise<boolean> {
|
||||
if (!values.jellyfin.baseUrl || !values.jellyfin.apiKey || reset) {
|
||||
jellyfinConnected = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return getJellyfinHealth(
|
||||
values.jellyfin.baseUrl || undefined,
|
||||
values.jellyfin.apiKey || undefined
|
||||
@@ -222,9 +263,9 @@
|
||||
{sonarrConnected}
|
||||
{radarrConnected}
|
||||
{jellyfinConnected}
|
||||
{checkSonarrHealth}
|
||||
{checkRadarrHealth}
|
||||
{checkJellyfinHealth}
|
||||
{updateSonarrHealth}
|
||||
{updateRadarrHealth}
|
||||
{updateJellyfinHealth}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
export let radarrConnected: boolean;
|
||||
export let jellyfinConnected: boolean;
|
||||
|
||||
export let checkSonarrHealth: () => Promise<boolean>;
|
||||
export let checkRadarrHealth: () => Promise<boolean>;
|
||||
export let checkJellyfinHealth: () => Promise<boolean>;
|
||||
export let updateSonarrHealth: (reset?: boolean) => Promise<boolean>;
|
||||
export let updateRadarrHealth: (reset?: boolean) => Promise<boolean>;
|
||||
export let updateJellyfinHealth: (reset?: boolean) => Promise<boolean>;
|
||||
|
||||
let sonarrRootFolders: undefined | { id: number; path: string }[] = undefined;
|
||||
let sonarrQualityProfiles: undefined | { id: number; name: string }[] = undefined;
|
||||
@@ -40,16 +40,16 @@
|
||||
values.sonarr.baseUrl = '';
|
||||
values.sonarr.apiKey = '';
|
||||
|
||||
checkSonarrHealth();
|
||||
updateSonarrHealth();
|
||||
} else if (service === 'radarr') {
|
||||
values.radarr.baseUrl = '';
|
||||
values.radarr.apiKey = '';
|
||||
checkRadarrHealth();
|
||||
updateRadarrHealth();
|
||||
} else if (service === 'jellyfin') {
|
||||
values.jellyfin.baseUrl = '';
|
||||
values.jellyfin.apiKey = '';
|
||||
values.jellyfin.userId = '';
|
||||
checkJellyfinHealth();
|
||||
updateJellyfinHealth();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,16 +135,21 @@
|
||||
placeholder={'http://127.0.0.1:8989'}
|
||||
class="w-full"
|
||||
bind:value={values.sonarr.baseUrl}
|
||||
on:change={() => updateSonarrHealth(true)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-sm text-zinc-500">
|
||||
{$_('settings.integrations.apiKey')}
|
||||
</h2>
|
||||
<Input class="w-full" bind:value={values.sonarr.apiKey} />
|
||||
<Input
|
||||
class="w-full"
|
||||
bind:value={values.sonarr.apiKey}
|
||||
on:change={() => updateSonarrHealth(true)}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-[1fr_min-content] gap-2">
|
||||
<TestConnectionButton handleHealthCheck={checkSonarrHealth} />
|
||||
<TestConnectionButton handleHealthCheck={updateSonarrHealth} />
|
||||
<FormButton on:click={() => handleRemoveIntegration('sonarr')} type="error">
|
||||
<Trash size={20} />
|
||||
</FormButton>
|
||||
@@ -216,16 +221,21 @@
|
||||
placeholder={'http://127.0.0.1:7878'}
|
||||
class="w-full"
|
||||
bind:value={values.radarr.baseUrl}
|
||||
on:change={() => updateSonarrHealth(true)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-sm text-zinc-500">
|
||||
{$_('settings.integrations.apiKey')}
|
||||
</h2>
|
||||
<Input class="w-full" bind:value={values.radarr.apiKey} />
|
||||
<Input
|
||||
class="w-full"
|
||||
bind:value={values.radarr.apiKey}
|
||||
on:change={() => updateSonarrHealth(true)}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-[1fr_min-content] gap-2">
|
||||
<TestConnectionButton handleHealthCheck={checkRadarrHealth} />
|
||||
<TestConnectionButton handleHealthCheck={updateRadarrHealth} />
|
||||
<FormButton on:click={() => handleRemoveIntegration('radarr')} type="error">
|
||||
<Trash size={20} />
|
||||
</FormButton>
|
||||
@@ -284,16 +294,21 @@
|
||||
placeholder={'http://127.0.0.1:8096'}
|
||||
class="w-full"
|
||||
bind:value={values.jellyfin.baseUrl}
|
||||
on:change={() => updateSonarrHealth(true)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-sm text-zinc-500">
|
||||
{$_('settings.integrations.apiKey')}
|
||||
</h2>
|
||||
<Input class="w-full" bind:value={values.jellyfin.apiKey} />
|
||||
<Input
|
||||
class="w-full"
|
||||
bind:value={values.jellyfin.apiKey}
|
||||
on:change={() => updateSonarrHealth(true)}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-[1fr_min-content] gap-2">
|
||||
<TestConnectionButton handleHealthCheck={checkJellyfinHealth} />
|
||||
<TestConnectionButton handleHealthCheck={updateJellyfinHealth} />
|
||||
<FormButton on:click={() => handleRemoveIntegration('jellyfin')} type="error">
|
||||
<Trash size={20} />
|
||||
</FormButton>
|
||||
|
||||
Reference in New Issue
Block a user