feat: Notify users about invalid and incomplete settings

This commit is contained in:
Aleksi Lassila
2023-08-21 18:17:10 +03:00
parent f67bf445c5
commit 8b520ef82f
8 changed files with 134 additions and 43 deletions

View File

@@ -33,8 +33,6 @@ function getRadarrApi() {
if (!baseUrl || !apiKey || !rootFolder || !qualityProfileId) return undefined;
console.log(baseUrl, apiKey);
return createClient<paths>({
baseUrl,
headers: {

View File

@@ -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}>

View File

@@ -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}

View File

@@ -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
})

View File

@@ -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": {

View File

@@ -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
});
}

View File

@@ -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>

View File

@@ -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>