mirror of
https://github.com/sct/overseerr.git
synced 2025-09-27 12:39:46 +02:00
feat: sonarr edit/delete modal
This commit is contained in:
@@ -1049,6 +1049,50 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/SonarrSettings'
|
$ref: '#/components/schemas/SonarrSettings'
|
||||||
|
/settings/sonarr/test:
|
||||||
|
post:
|
||||||
|
summary: Test Sonarr configuration
|
||||||
|
description: Test if the provided Sonarr congifuration values are valid. Returns profiles and root folders on success
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
hostname:
|
||||||
|
type: string
|
||||||
|
example: '127.0.0.1'
|
||||||
|
port:
|
||||||
|
type: number
|
||||||
|
example: 8989
|
||||||
|
apiKey:
|
||||||
|
type: string
|
||||||
|
example: yourapikey
|
||||||
|
useSsl:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
baseUrl:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- hostname
|
||||||
|
- port
|
||||||
|
- apiKey
|
||||||
|
- useSsl
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Succesfully connected to Sonarr instance
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
profiles:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ServiceProfile'
|
||||||
/settings/sonarr/{sonarrId}:
|
/settings/sonarr/{sonarrId}:
|
||||||
put:
|
put:
|
||||||
summary: Update existing sonarr instance
|
summary: Update existing sonarr instance
|
||||||
|
@@ -3,6 +3,7 @@ import Axios, { AxiosInstance } from 'axios';
|
|||||||
interface RadarrMovieOptions {
|
interface RadarrMovieOptions {
|
||||||
title: string;
|
title: string;
|
||||||
qualityProfileId: number;
|
qualityProfileId: number;
|
||||||
|
minimumAvailability: string;
|
||||||
profileId: number;
|
profileId: number;
|
||||||
year: number;
|
year: number;
|
||||||
rootFolderPath: string;
|
rootFolderPath: string;
|
||||||
@@ -83,6 +84,7 @@ class RadarrAPI {
|
|||||||
qualityProfileId: options.qualityProfileId,
|
qualityProfileId: options.qualityProfileId,
|
||||||
profileId: options.profileId,
|
profileId: options.profileId,
|
||||||
titleSlug: options.tmdbId.toString(),
|
titleSlug: options.tmdbId.toString(),
|
||||||
|
minimumAvailability: options.minimumAvailability,
|
||||||
tmdbId: options.tmdbId,
|
tmdbId: options.tmdbId,
|
||||||
year: options.year,
|
year: options.year,
|
||||||
rootFolderPath: options.rootFolderPath,
|
rootFolderPath: options.rootFolderPath,
|
||||||
|
@@ -155,6 +155,7 @@ export class MediaRequest {
|
|||||||
profileId: radarrSettings.activeProfileId,
|
profileId: radarrSettings.activeProfileId,
|
||||||
qualityProfileId: radarrSettings.activeProfileId,
|
qualityProfileId: radarrSettings.activeProfileId,
|
||||||
rootFolderPath: radarrSettings.activeDirectory,
|
rootFolderPath: radarrSettings.activeDirectory,
|
||||||
|
minimumAvailability: radarrSettings.minimumAvailability,
|
||||||
title: movie.title,
|
title: movie.title,
|
||||||
tmdbId: movie.id,
|
tmdbId: movie.id,
|
||||||
year: Number(movie.release_date.slice(0, 4)),
|
year: Number(movie.release_date.slice(0, 4)),
|
||||||
|
@@ -254,6 +254,35 @@ settingsRoutes.post('/sonarr', (req, res) => {
|
|||||||
return res.status(201).json(newSonarr);
|
return res.status(201).json(newSonarr);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
settingsRoutes.post('/sonarr/test', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const sonarr = new SonarrAPI({
|
||||||
|
apiKey: req.body.apiKey,
|
||||||
|
url: `${req.body.useSsl ? 'https' : 'http'}://${req.body.hostname}:${
|
||||||
|
req.body.port
|
||||||
|
}${req.body.baseUrl ?? ''}/api`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const profiles = await sonarr.getProfiles();
|
||||||
|
const folders = await sonarr.getRootFolders();
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
profiles,
|
||||||
|
rootFolders: folders.map((folder) => ({
|
||||||
|
id: folder.id,
|
||||||
|
path: folder.path,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to test Sonarr', {
|
||||||
|
label: 'Sonarr',
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
next({ status: 500, message: 'Failed to connect to Sonarr' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
settingsRoutes.put<{ id: string }>('/sonarr/:id', (req, res) => {
|
settingsRoutes.put<{ id: string }>('/sonarr/:id', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
|
@@ -240,7 +240,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
|||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
type="input"
|
type="input"
|
||||||
placeholder="127.0.0.1"
|
placeholder="A Radarr Server"
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setIsValidated(false);
|
setIsValidated(false);
|
||||||
setFieldValue('name', e.target.value);
|
setFieldValue('name', e.target.value);
|
||||||
@@ -462,11 +462,6 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
|
|||||||
<option value="preDB">PreDB</option>
|
<option value="preDB">PreDB</option>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
{errors.rootFolder && touched.rootFolder && (
|
|
||||||
<div className="text-red-500 mt-2">
|
|
||||||
{errors.rootFolder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
|
@@ -12,6 +12,7 @@ import RadarrModal from './RadarrModal';
|
|||||||
import Modal from '../Common/Modal';
|
import Modal from '../Common/Modal';
|
||||||
import Transition from '../Transition';
|
import Transition from '../Transition';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import SonarrModal from './SonarrModal';
|
||||||
|
|
||||||
interface ServerInstanceProps {
|
interface ServerInstanceProps {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -112,9 +113,11 @@ const SettingsServices: React.FC = () => {
|
|||||||
error: radarrError,
|
error: radarrError,
|
||||||
revalidate: revalidateRadarr,
|
revalidate: revalidateRadarr,
|
||||||
} = useSWR<RadarrSettings[]>('/api/v1/settings/radarr');
|
} = useSWR<RadarrSettings[]>('/api/v1/settings/radarr');
|
||||||
const { data: sonarrData, error: sonarrError } = useSWR<SonarrSettings[]>(
|
const {
|
||||||
'/api/v1/settings/sonarr'
|
data: sonarrData,
|
||||||
);
|
error: sonarrError,
|
||||||
|
revalidate: revalidateSonarr,
|
||||||
|
} = useSWR<SonarrSettings[]>('/api/v1/settings/sonarr');
|
||||||
const [editRadarrModal, setEditRadarrModal] = useState<{
|
const [editRadarrModal, setEditRadarrModal] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
radarr: RadarrSettings | null;
|
radarr: RadarrSettings | null;
|
||||||
@@ -122,18 +125,30 @@ const SettingsServices: React.FC = () => {
|
|||||||
open: false,
|
open: false,
|
||||||
radarr: null,
|
radarr: null,
|
||||||
});
|
});
|
||||||
const [deleteRadarrModal, setDeleteRadarrModal] = useState<{
|
const [editSonarrModal, setEditSonarrModal] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
radarrId: number | null;
|
sonarr: SonarrSettings | null;
|
||||||
}>({
|
}>({
|
||||||
open: false,
|
open: false,
|
||||||
radarrId: null,
|
sonarr: null,
|
||||||
|
});
|
||||||
|
const [deleteServerModal, setDeleteServerModal] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
type: 'radarr' | 'sonarr';
|
||||||
|
serverId: number | null;
|
||||||
|
}>({
|
||||||
|
open: false,
|
||||||
|
type: 'radarr',
|
||||||
|
serverId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteServer = async (radarrId: number) => {
|
const deleteServer = async () => {
|
||||||
await axios.delete(`/api/v1/settings/radarr/${radarrId}`);
|
await axios.delete(
|
||||||
setDeleteRadarrModal({ open: false, radarrId: null });
|
`/api/v1/settings/${deleteServerModal.type}/${deleteServerModal.serverId}`
|
||||||
|
);
|
||||||
|
setDeleteServerModal({ open: false, serverId: null, type: 'radarr' });
|
||||||
revalidateRadarr();
|
revalidateRadarr();
|
||||||
|
revalidateSonarr();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -159,8 +174,18 @@ const SettingsServices: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{editSonarrModal.open && (
|
||||||
|
<SonarrModal
|
||||||
|
sonarr={editSonarrModal.sonarr}
|
||||||
|
onClose={() => setEditSonarrModal({ open: false, sonarr: null })}
|
||||||
|
onSave={() => {
|
||||||
|
revalidateSonarr();
|
||||||
|
setEditSonarrModal({ open: false, sonarr: null });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Transition
|
<Transition
|
||||||
show={deleteRadarrModal.open}
|
show={deleteServerModal.open}
|
||||||
enter="transition ease-in-out duration-300 transform opacity-0"
|
enter="transition ease-in-out duration-300 transform opacity-0"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
enterTo="opacuty-100"
|
enterTo="opacuty-100"
|
||||||
@@ -171,11 +196,17 @@ const SettingsServices: React.FC = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
okText="Delete"
|
okText="Delete"
|
||||||
okButtonType="danger"
|
okButtonType="danger"
|
||||||
onOk={() => deleteServer(deleteRadarrModal.radarrId ?? 0)}
|
onOk={() => deleteServer()}
|
||||||
onCancel={() => setDeleteRadarrModal({ open: false, radarrId: null })}
|
onCancel={() =>
|
||||||
|
setDeleteServerModal({
|
||||||
|
open: false,
|
||||||
|
serverId: null,
|
||||||
|
type: 'radarr',
|
||||||
|
})
|
||||||
|
}
|
||||||
title="Delete Server"
|
title="Delete Server"
|
||||||
>
|
>
|
||||||
Are you sure you want to delete this Radarr server?
|
Are you sure you want to delete this server?
|
||||||
</Modal>
|
</Modal>
|
||||||
</Transition>
|
</Transition>
|
||||||
<div className="mt-6 sm:mt-5">
|
<div className="mt-6 sm:mt-5">
|
||||||
@@ -193,7 +224,11 @@ const SettingsServices: React.FC = () => {
|
|||||||
isDefault4K={radarr.is4k && radarr.isDefault}
|
isDefault4K={radarr.is4k && radarr.isDefault}
|
||||||
onEdit={() => setEditRadarrModal({ open: true, radarr })}
|
onEdit={() => setEditRadarrModal({ open: true, radarr })}
|
||||||
onDelete={() =>
|
onDelete={() =>
|
||||||
setDeleteRadarrModal({ open: true, radarrId: radarr.id })
|
setDeleteServerModal({
|
||||||
|
open: true,
|
||||||
|
serverId: radarr.id,
|
||||||
|
type: 'radarr',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -244,10 +279,19 @@ const SettingsServices: React.FC = () => {
|
|||||||
key={`sonarr-config-${sonarr.id}`}
|
key={`sonarr-config-${sonarr.id}`}
|
||||||
name={sonarr.name}
|
name={sonarr.name}
|
||||||
address={sonarr.hostname}
|
address={sonarr.hostname}
|
||||||
profileName={sonarr.activeProfileId.toString()}
|
profileName={sonarr.activeProfileName}
|
||||||
isSSL={sonarr.useSsl}
|
isSSL={sonarr.useSsl}
|
||||||
onEdit={() => console.log('nada')}
|
isSonarr
|
||||||
onDelete={() => console.log('delete clicked')}
|
isDefault4K={sonarr.isDefault && sonarr.is4k}
|
||||||
|
isDefault={sonarr.isDefault && !sonarr.is4k}
|
||||||
|
onEdit={() => setEditSonarrModal({ open: true, sonarr })}
|
||||||
|
onDelete={() =>
|
||||||
|
setDeleteServerModal({
|
||||||
|
open: true,
|
||||||
|
serverId: sonarr.id,
|
||||||
|
type: 'sonarr',
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<li className="col-span-1 border-2 border-dashed border-cool-gray-400 rounded-lg shadow h-32 sm:h-32">
|
<li className="col-span-1 border-2 border-dashed border-cool-gray-400 rounded-lg shadow h-32 sm:h-32">
|
||||||
@@ -255,7 +299,7 @@ const SettingsServices: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
buttonType="ghost"
|
buttonType="ghost"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setEditRadarrModal({ open: true, radarr: null })
|
setEditSonarrModal({ open: true, sonarr: null })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
487
src/components/Settings/SonarrModal.tsx
Normal file
487
src/components/Settings/SonarrModal.tsx
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import Transition from '../Transition';
|
||||||
|
import Modal from '../Common/Modal';
|
||||||
|
import { Formik, Field } from 'formik';
|
||||||
|
import type { SonarrSettings } from '../../../server/lib/settings';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
|
||||||
|
interface TestResponse {
|
||||||
|
profiles: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
rootFolders: {
|
||||||
|
id: number;
|
||||||
|
path: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SonarrModalProps {
|
||||||
|
sonarr: SonarrSettings | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SonarrModal: React.FC<SonarrModalProps> = ({
|
||||||
|
onClose,
|
||||||
|
sonarr,
|
||||||
|
onSave,
|
||||||
|
}) => {
|
||||||
|
const initialLoad = useRef(false);
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
const [isValidated, setIsValidated] = useState(sonarr ? true : false);
|
||||||
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const [testResponse, setTestResponse] = useState<TestResponse>({
|
||||||
|
profiles: [],
|
||||||
|
rootFolders: [],
|
||||||
|
});
|
||||||
|
const SonarrSettingsSchema = Yup.object().shape({
|
||||||
|
hostname: Yup.string().required('You must provide a hostname/IP'),
|
||||||
|
port: Yup.number().required('You must provide a port'),
|
||||||
|
apiKey: Yup.string().required('You must provide an API Key'),
|
||||||
|
rootFolder: Yup.string().required('You must select a root folder'),
|
||||||
|
activeProfileId: Yup.string().required('You must select a profile'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const testConnection = useCallback(
|
||||||
|
async ({
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
apiKey,
|
||||||
|
baseUrl,
|
||||||
|
useSsl = false,
|
||||||
|
}: {
|
||||||
|
hostname: string;
|
||||||
|
port: number;
|
||||||
|
apiKey: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
useSsl?: boolean;
|
||||||
|
}) => {
|
||||||
|
setIsTesting(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.post<TestResponse>(
|
||||||
|
'/api/v1/settings/sonarr/test',
|
||||||
|
{
|
||||||
|
hostname,
|
||||||
|
apiKey,
|
||||||
|
port,
|
||||||
|
baseUrl,
|
||||||
|
useSsl,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsValidated(true);
|
||||||
|
setTestResponse(response.data);
|
||||||
|
if (initialLoad.current) {
|
||||||
|
addToast('Sonarr connection established!', {
|
||||||
|
appearance: 'success',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setIsValidated(false);
|
||||||
|
if (initialLoad.current) {
|
||||||
|
addToast('Failed to connect to Sonarr server', {
|
||||||
|
appearance: 'error',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsTesting(false);
|
||||||
|
initialLoad.current = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addToast]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sonarr) {
|
||||||
|
testConnection({
|
||||||
|
apiKey: sonarr.apiKey,
|
||||||
|
hostname: sonarr.hostname,
|
||||||
|
port: sonarr.port,
|
||||||
|
baseUrl: sonarr.baseUrl,
|
||||||
|
useSsl: sonarr.useSsl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [sonarr, testConnection]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
appear
|
||||||
|
show
|
||||||
|
enter="transition ease-in-out duration-300 transform opacity-0"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacuty-100"
|
||||||
|
leave="transition ease-in-out duration-300 transform opacity-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
name: sonarr?.name,
|
||||||
|
hostname: sonarr?.hostname,
|
||||||
|
port: sonarr?.port,
|
||||||
|
ssl: sonarr?.useSsl ?? false,
|
||||||
|
apiKey: sonarr?.apiKey,
|
||||||
|
baseUrl: sonarr?.baseUrl,
|
||||||
|
activeProfileId: sonarr?.activeProfileId,
|
||||||
|
rootFolder: sonarr?.activeDirectory,
|
||||||
|
isDefault: sonarr?.isDefault ?? false,
|
||||||
|
is4k: sonarr?.is4k ?? false,
|
||||||
|
enableSeasonFolders: sonarr?.enableSeasonFolders ?? false,
|
||||||
|
}}
|
||||||
|
validationSchema={SonarrSettingsSchema}
|
||||||
|
onSubmit={async (values) => {
|
||||||
|
try {
|
||||||
|
const profileName = testResponse.profiles.find(
|
||||||
|
(profile) => profile.id === Number(values.activeProfileId)
|
||||||
|
)?.name;
|
||||||
|
|
||||||
|
const submission = {
|
||||||
|
name: values.name,
|
||||||
|
hostname: values.hostname,
|
||||||
|
port: values.port,
|
||||||
|
apiKey: values.apiKey,
|
||||||
|
useSsl: values.ssl,
|
||||||
|
baseUrl: values.baseUrl,
|
||||||
|
activeProfileId: values.activeProfileId,
|
||||||
|
activeProfileName: profileName,
|
||||||
|
activeDirectory: values.rootFolder,
|
||||||
|
is4k: values.is4k,
|
||||||
|
isDefault: values.isDefault,
|
||||||
|
enableSeasonFolders: values.enableSeasonFolders,
|
||||||
|
};
|
||||||
|
if (!sonarr) {
|
||||||
|
await axios.post('/api/v1/settings/sonarr', submission);
|
||||||
|
} else {
|
||||||
|
await axios.put(
|
||||||
|
`/api/v1/settings/sonarr/${sonarr.id}`,
|
||||||
|
submission
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave();
|
||||||
|
} catch (e) {
|
||||||
|
// set error here
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
values,
|
||||||
|
handleSubmit,
|
||||||
|
setFieldValue,
|
||||||
|
isSubmitting,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
onCancel={onClose}
|
||||||
|
okButtonType="primary"
|
||||||
|
okText={
|
||||||
|
isSubmitting
|
||||||
|
? 'Saving...'
|
||||||
|
: !!sonarr
|
||||||
|
? 'Save Changes'
|
||||||
|
: 'Create Instance'
|
||||||
|
}
|
||||||
|
secondaryButtonType="warning"
|
||||||
|
secondaryText={isTesting ? 'Testing...' : 'Test'}
|
||||||
|
onSecondary={() => {
|
||||||
|
if (values.apiKey && values.hostname && values.port) {
|
||||||
|
testConnection({
|
||||||
|
apiKey: values.apiKey,
|
||||||
|
baseUrl: values.baseUrl,
|
||||||
|
hostname: values.hostname,
|
||||||
|
port: values.port,
|
||||||
|
useSsl: values.ssl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
secondaryDisabled={
|
||||||
|
!values.apiKey || !values.hostname || !values.port || isTesting
|
||||||
|
}
|
||||||
|
okDisabled={!isValidated || isSubmitting || isTesting}
|
||||||
|
onOk={() => handleSubmit()}
|
||||||
|
title={
|
||||||
|
!sonarr ? 'Create New Sonarr Server' : 'Edit Sonarr Server'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="isDefault"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Default Server
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="isDefault"
|
||||||
|
name="isDefault"
|
||||||
|
className="form-checkbox h-6 w-6 text-indigo-600 transition duration-150 ease-in-out"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Server Name
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="input"
|
||||||
|
placeholder="A Sonarr Server"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIsValidated(false);
|
||||||
|
setFieldValue('name', e.target.value);
|
||||||
|
}}
|
||||||
|
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.name && touched.name && (
|
||||||
|
<div className="text-red-500 mt-2">{errors.name}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="hostname"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Hostname
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
id="hostname"
|
||||||
|
name="hostname"
|
||||||
|
type="input"
|
||||||
|
placeholder="127.0.0.1"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIsValidated(false);
|
||||||
|
setFieldValue('hostname', e.target.value);
|
||||||
|
}}
|
||||||
|
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.hostname && touched.hostname && (
|
||||||
|
<div className="text-red-500 mt-2">{errors.hostname}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="port"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Port
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
id="port"
|
||||||
|
name="port"
|
||||||
|
type="input"
|
||||||
|
placeholder="8989"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIsValidated(false);
|
||||||
|
setFieldValue('port', e.target.value);
|
||||||
|
}}
|
||||||
|
className="rounded-md shadow-sm form-input block w-24 transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
|
||||||
|
/>
|
||||||
|
{errors.port && touched.port && (
|
||||||
|
<div className="text-red-500 mt-2">{errors.port}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="ssl"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
SSL
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="ssl"
|
||||||
|
name="ssl"
|
||||||
|
onChange={() => {
|
||||||
|
setIsValidated(false);
|
||||||
|
setFieldValue('ssl', !values.ssl);
|
||||||
|
}}
|
||||||
|
className="form-checkbox h-6 w-6 text-indigo-600 transition duration-150 ease-in-out"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="apiKey"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
id="apiKey"
|
||||||
|
name="apiKey"
|
||||||
|
type="input"
|
||||||
|
placeholder="Your Sonarr API Key"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIsValidated(false);
|
||||||
|
setFieldValue('apiKey', e.target.value);
|
||||||
|
}}
|
||||||
|
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.apiKey && touched.apiKey && (
|
||||||
|
<div className="text-red-500 mt-2">{errors.apiKey}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="baseUrl"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Base URL
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
id="baseUrl"
|
||||||
|
name="baseUrl"
|
||||||
|
type="input"
|
||||||
|
placeholder="Example: /sonarr"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setIsValidated(false);
|
||||||
|
setFieldValue('baseUrl', e.target.value);
|
||||||
|
}}
|
||||||
|
className="flex-1 form-input block w-full min-w-0 rounded-md transition duration-150 ease-in-out sm:text-sm sm:leading-5 bg-cool-gray-700 border border-cool-gray-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{errors.baseUrl && touched.baseUrl && (
|
||||||
|
<div className="text-red-500 mt-2">{errors.baseUrl}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="activeProfileId"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Quality Profile
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
as="select"
|
||||||
|
id="activeProfileId"
|
||||||
|
name="activeProfileId"
|
||||||
|
className="mt-1 form-select block w-full pl-3 pr-10 py-2 text-base leading-6 bg-cool-gray-700 border-cool-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-cool-gray-500 sm:text-sm sm:leading-5"
|
||||||
|
>
|
||||||
|
<option value="">Select a Quality Profile</option>
|
||||||
|
{testResponse.profiles.length > 0 &&
|
||||||
|
testResponse.profiles.map((profile) => (
|
||||||
|
<option
|
||||||
|
key={`loaded-profile-${profile.id}`}
|
||||||
|
value={profile.id}
|
||||||
|
>
|
||||||
|
{profile.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
{errors.activeProfileId && touched.activeProfileId && (
|
||||||
|
<div className="text-red-500 mt-2">
|
||||||
|
{errors.activeProfileId}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-800 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="rootFolder"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Root Folder
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<div className="max-w-lg flex rounded-md shadow-sm">
|
||||||
|
<Field
|
||||||
|
as="select"
|
||||||
|
id="rootFolder"
|
||||||
|
name="rootFolder"
|
||||||
|
className="mt-1 form-select block w-full pl-3 pr-10 py-2 text-base leading-6 bg-cool-gray-700 border-cool-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-cool-gray-500 sm:text-sm sm:leading-5"
|
||||||
|
>
|
||||||
|
<option value="">Select a Root Folder</option>
|
||||||
|
{testResponse.rootFolders.length > 0 &&
|
||||||
|
testResponse.rootFolders.map((folder) => (
|
||||||
|
<option
|
||||||
|
key={`loaded-profile-${folder.id}`}
|
||||||
|
value={folder.path}
|
||||||
|
>
|
||||||
|
{folder.path}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
{errors.rootFolder && touched.rootFolder && (
|
||||||
|
<div className="text-red-500 mt-2">
|
||||||
|
{errors.rootFolder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="is4k"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Ultra HD Server
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="is4k"
|
||||||
|
name="is4k"
|
||||||
|
className="form-checkbox h-6 w-6 text-indigo-600 transition duration-150 ease-in-out"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 sm:mt-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
||||||
|
<label
|
||||||
|
htmlFor="enableSeasonFolders"
|
||||||
|
className="block text-sm font-medium leading-5 text-cool-gray-400 sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
Season Folders
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 sm:mt-0 sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="enableSeasonFolders"
|
||||||
|
name="enableSeasonFolders"
|
||||||
|
className="form-checkbox h-6 w-6 text-indigo-600 transition duration-150 ease-in-out"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Formik>
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SonarrModal;
|
Reference in New Issue
Block a user