feat: radarr edit/create modal/backend functionality

pull/124/head
sct 4 years ago
parent e032e385a5
commit c4ac357ef4

@ -116,6 +116,9 @@ components:
activeProfileId:
type: number
example: 1
activeProfileName:
type: string
example: 720p/1080p
activeDirectory:
type: string
example: '/movies'
@ -125,16 +128,21 @@ components:
minimumAvailability:
type: string
example: 'In Cinema'
isDefault:
type: boolean
example: false
required:
- name
- hostname
- port
- apiKey
- useSsl
- activeProfile
- activeProfileId
- activeProfileName
- activeDirectory
- is4k
- minimumAvailability
- isDefault
SonarrSettings:
type: object
properties:
@ -162,12 +170,18 @@ components:
activeProfileId:
type: number
example: 1
activeProfileName:
type: string
example: 720p/1080p
activeDirectory:
type: string
example: '/tv/'
activeAnimeProfileId:
type: number
nullable: true
activeAnimeProfileName:
type: string
example: 720p/1080p
activeAnimeDirectory:
type: string
nullable: true
@ -177,6 +191,9 @@ components:
enableSeasonFolders:
type: boolean
example: false
isDefault:
type: boolean
example: false
required:
- name
- hostname
@ -184,9 +201,11 @@ components:
- apiKey
- useSsl
- activeProfileId
- activeProfileName
- activeDirectory
- is4k
- enableSeasonFolders
- isDefault
PublicSettings:
type: object
properties:
@ -704,6 +723,15 @@ components:
type: string
twitterId:
type: string
ServiceProfile:
type: object
properties:
id:
type: number
example: 1
name:
type: string
example: 720p/1080p
securitySchemes:
cookieAuth:
@ -877,6 +905,50 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/RadarrSettings'
/settings/radarr/test:
post:
summary: Test radarr configuration
description: Test if the provided Radarr 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: 7878
apiKey:
type: string
example: yourapikey
useSsl:
type: boolean
example: false
baseUrl:
type: string
required:
- hostname
- port
- apiKey
- useSsl
responses:
'200':
description: Succesfully connected to Radarr instance
content:
application/json:
schema:
type: object
properties:
profiles:
type: array
items:
$ref: '#/components/schemas/ServiceProfile'
/settings/radarr/{radarrId}:
put:
summary: Update existing radarr instance
@ -922,6 +994,28 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/RadarrSettings'
/settings/radarr/{radarrId}/profiles:
get:
summary: Retrieve available profiles for the Radarr instance
description: Returns an array of profile available on the Radarr server instance in JSON format
tags:
- settings
parameters:
- in: path
name: radarrId
required: true
schema:
type: integer
description: Radarr Instance ID
responses:
'200':
description: Returned list of profiles
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ServiceProfile'
/settings/sonarr:
get:
summary: Get all sonarr settings

@ -45,7 +45,8 @@
"uuid": "^8.3.0",
"winston": "^3.3.3",
"xml2js": "^0.4.23",
"yamljs": "^0.3.0"
"yamljs": "^0.3.0",
"yup": "^0.29.3"
},
"devDependencies": {
"@babel/cli": "^7.11.6",
@ -65,6 +66,7 @@
"@types/uuid": "^8.3.0",
"@types/xml2js": "^0.4.5",
"@types/yamljs": "^0.2.31",
"@types/yup": "^0.29.9",
"@typescript-eslint/eslint-plugin": "^4.0.0",
"@typescript-eslint/parser": "^3.10.1",
"babel-plugin-react-intl": "^8.2.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@ -25,8 +25,10 @@ interface DVRSettings {
useSsl: boolean;
baseUrl?: string;
activeProfileId: number;
activeProfileName: string;
activeDirectory: string;
is4k: boolean;
isDefault: boolean;
}
export interface RadarrSettings extends DVRSettings {
@ -35,6 +37,7 @@ export interface RadarrSettings extends DVRSettings {
export interface SonarrSettings extends DVRSettings {
activeAnimeProfileId?: number;
activeAnimeProfileName?: string;
activeAnimeDirectory?: string;
enableSeasonFolders: boolean;
}

@ -9,6 +9,9 @@ import { getRepository } from 'typeorm';
import { User } from '../entity/User';
import PlexAPI, { PlexLibrary } from '../api/plexapi';
import jobPlexSync from '../job/plexsync';
import SonarrAPI from '../api/sonarr';
import RadarrAPI from '../api/radarr';
import logger from '../logger';
const settingsRoutes = Router();
@ -132,6 +135,35 @@ settingsRoutes.post('/radarr', (req, res) => {
return res.status(201).json(newRadarr);
});
settingsRoutes.post('/radarr/test', async (req, res, next) => {
try {
const radarr = new RadarrAPI({
apiKey: req.body.apiKey,
url: `${req.body.useSsl ? 'https' : 'http'}://${req.body.hostname}:${
req.body.port
}${req.body.baseUrl ?? ''}/api`,
});
const profiles = await radarr.getProfiles();
const folders = await radarr.getRootFolders();
return res.status(200).json({
profiles,
rootFolders: folders.map((folder) => ({
id: folder.id,
path: folder.path,
})),
});
} catch (e) {
logger.error('Failed to test Radarr', {
label: 'Radarr',
message: e.message,
});
next({ status: 500, message: 'Failed to connect to Radarr' });
}
});
settingsRoutes.put<{ id: string }>('/radarr/:id', (req, res) => {
const settings = getSettings();
@ -154,6 +186,36 @@ settingsRoutes.put<{ id: string }>('/radarr/:id', (req, res) => {
return res.status(200).json(settings.radarr[radarrIndex]);
});
settingsRoutes.get<{ id: string }>('/radarr/:id/profiles', async (req, res) => {
const settings = getSettings();
const radarrSettings = settings.radarr.find(
(r) => r.id === Number(req.params.id)
);
if (!radarrSettings) {
return res
.status(404)
.json({ status: '404', message: 'Settings instance not found' });
}
const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey,
url: `${radarrSettings.useSsl ? 'https' : 'http'}://${
radarrSettings.hostname
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}/api`,
});
const profiles = await radarr.getProfiles();
return res.status(200).json(
profiles.map((profile) => ({
id: profile.id,
name: profile.name,
}))
);
});
settingsRoutes.delete<{ id: string }>('/radarr/:id', (req, res) => {
const settings = getSettings();

@ -5,7 +5,8 @@ export type ButtonType =
| 'primary'
| 'danger'
| 'warning'
| 'success';
| 'success'
| 'ghost';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
buttonType?: ButtonType;
@ -30,22 +31,27 @@ const Button: React.FC<ButtonProps> = ({
break;
case 'danger':
buttonStyle.push(
'text-white bg-red-600 hover:bg-red-500 focus:border-red-700 focus:shadow-outline-red active:bg-red-700'
'text-white bg-red-600 hover:bg-red-500 focus:border-red-700 focus:shadow-outline-red active:bg-red-700 disabled:opacity-50'
);
break;
case 'warning':
buttonStyle.push(
'text-white bg-orange-500 hover:bg-orange-400 focus:border-orange-700 focus:shadow-outline-orange active:bg-orange-700'
'text-white bg-orange-500 hover:bg-orange-400 focus:border-orange-700 focus:shadow-outline-orange active:bg-orange-700 disabled:opacity-50'
);
break;
case 'success':
buttonStyle.push(
'text-white bg-green-400 hover:bg-green-300 focus:border-green-700 focus:shadow-outline-green active:bg-green-700'
'text-white bg-green-400 hover:bg-green-300 focus:border-green-700 focus:shadow-outline-green active:bg-green-700 disabled:opacity-50'
);
break;
case 'ghost':
buttonStyle.push(
'text-white bg-transaprent border border-cool-gray-600 hover:border-cool-gray-200 focus:border-cool-gray-100 active:border-cool-gray-100 disabled:opacity-50'
);
break;
default:
buttonStyle.push(
'leading-5 font-medium rounded-md text-gray-200 bg-cool-gray-500 hover:bg-cool-gray-400 hover:text-white focus:border-blue-300 focus:shadow-outline-blue active:text-gray-200 active:bg-cool-gray-400'
'leading-5 font-medium rounded-md text-gray-200 bg-cool-gray-500 hover:bg-cool-gray-400 hover:text-white focus:border-blue-300 focus:shadow-outline-blue active:text-gray-200 active:bg-cool-gray-400 disabled:opacity-50'
);
}

@ -103,7 +103,7 @@ const Modal: React.FC<ModalProps> = ({
item && (
<animated.div
style={props}
className="inline-block align-bottom bg-cool-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6"
className="inline-block align-bottom bg-cool-gray-700 sm:rounded-lg px-4 pt-5 pb-4 text-left overflow-auto shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6 max-h-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
@ -116,7 +116,7 @@ const Modal: React.FC<ModalProps> = ({
{iconSvg}
</div>
)}
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<div className="mt-3 text-center sm:mt-0 sm:text-left mb-6">
{title && (
<h3
className="text-lg leading-6 font-medium text-white"
@ -128,10 +128,8 @@ const Modal: React.FC<ModalProps> = ({
</div>
</div>
{children && (
<div className="mt-4">
<p className="text-sm leading-5 text-cool-gray-300">
{children}
</p>
<div className="mt-4 text-sm leading-5 text-cool-gray-300">
{children}
</div>
)}
{(onCancel || onOk || onSecondary || onTertiary) && (

@ -8,6 +8,7 @@ import { useUser, Permission } from '../../../hooks/useUser';
const messages = defineMessages({
dashboard: 'Dashboard',
requests: 'Requests',
users: 'Users',
settings: 'Settings',
});
@ -68,6 +69,22 @@ const SidebarLinks: SidebarLinkProps[] = [
),
activeRegExp: /^\/requests/,
},
{
href: '/users',
messagesKey: 'users',
svgIcon: (
<svg
className="mr-3 h-6 w-6 text-gray-300 group-hover:text-gray-300 group-focus:text-gray-300 transition ease-in-out duration-150"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z" />
</svg>
),
activeRegExp: /^\/users/,
requiredPermission: Permission.MANAGE_USERS,
},
{
href: '/settings',
messagesKey: 'settings',

@ -0,0 +1,464 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import Transition from '../Transition';
import Modal from '../Common/Modal';
import { Formik, Field } from 'formik';
import type { RadarrSettings } 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 RadarrModalProps {
radarr: RadarrSettings | null;
onClose: () => void;
onSave: () => void;
}
const RadarrModal: React.FC<RadarrModalProps> = ({
onClose,
radarr,
onSave,
}) => {
const initialLoad = useRef(false);
const { addToast } = useToasts();
const [isValidated, setIsValidated] = useState(radarr ? true : false);
const [isTesting, setIsTesting] = useState(false);
const [testResponse, setTestResponse] = useState<TestResponse>({
profiles: [],
rootFolders: [],
});
const RadarrSettingsSchema = 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/radarr/test',
{
hostname,
apiKey,
port,
baseUrl,
useSsl,
}
);
setIsValidated(true);
setTestResponse(response.data);
if (initialLoad.current) {
addToast('Radarr connection established!', {
appearance: 'success',
autoDismiss: true,
});
}
} catch (e) {
setIsValidated(false);
if (initialLoad.current) {
addToast('Failed to connect to Radarr server', {
appearance: 'error',
autoDismiss: true,
});
}
} finally {
setIsTesting(false);
initialLoad.current = true;
}
},
[addToast]
);
useEffect(() => {
if (radarr) {
testConnection({
apiKey: radarr.apiKey,
hostname: radarr.hostname,
port: radarr.port,
baseUrl: radarr.baseUrl,
useSsl: radarr.useSsl,
});
}
}, [radarr, 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: radarr?.name,
hostname: radarr?.hostname,
port: radarr?.port,
ssl: radarr?.useSsl ?? false,
apiKey: radarr?.apiKey,
baseUrl: radarr?.baseUrl,
activeProfileId: radarr?.activeProfileId,
rootFolder: radarr?.activeDirectory,
isDefault: radarr?.isDefault ?? false,
is4k: radarr?.is4k ?? false,
}}
validationSchema={RadarrSettingsSchema}
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,
minimumAvailability: 'In Cinema',
isDefault: values.isDefault,
};
if (!radarr) {
await axios.post('/api/v1/settings/radarr', submission);
} else {
await axios.put(
`/api/v1/settings/radarr/${radarr.id}`,
submission
);
}
onSave();
} catch (e) {
// set error here
}
}}
>
{({
errors,
touched,
values,
handleSubmit,
setFieldValue,
isSubmitting,
}) => {
return (
<Modal
onCancel={onClose}
okButtonType="primary"
okText={
isSubmitting
? 'Saving...'
: !!radarr
? '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={
!radarr ? 'Create New Radarr Instance' : 'Edit Radarr Instance'
}
>
<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="port"
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="127.0.0.1"
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="7878"
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="port"
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="name"
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 Radarr 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="name"
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="name"
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"
>
{testResponse.profiles.length > 0 &&
testResponse.profiles.map((profile) => (
<option
key={`loaded-profile-${profile.id}`}
value={profile.id}
>
{profile.name}
</option>
))}
</Field>
</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="name"
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"
>
{testResponse.rootFolders.length > 0 &&
testResponse.rootFolders.map((folder) => (
<option
key={`loaded-profile-${folder.id}`}
value={folder.path}
>
{folder.path}
</option>
))}
</Field>
</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-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"
>
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>
</Modal>
);
}}
</Formik>
</Transition>
);
};
export default RadarrModal;

@ -285,10 +285,10 @@ const SettingsPlex: React.FC = () => {
</p>
<div className="mt-6">
<div className="bg-cool-gray-800 p-4 rounded-md">
<div className="w-full h-8 rounded-full bg-cool-gray-600 mb-6 relative">
<div className="w-full h-8 rounded-full bg-cool-gray-600 mb-6 relative overflow-hidden">
{dataSync?.running && (
<div
className="h-8 rounded-full bg-indigo-600 transition-all ease-in-out duration-200"
className="h-8 bg-indigo-600 transition-all ease-in-out duration-200"
style={{
width: `${Math.round(
(dataSync.progress / dataSync.total) * 100

@ -0,0 +1,247 @@
import React, { useState } from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import Badge from '../Common/Badge';
import Button from '../Common/Button';
import useSWR from 'swr';
import type {
RadarrSettings,
SonarrSettings,
} from '../../../server/lib/settings';
import LoadingSpinner from '../Common/LoadingSpinner';
import RadarrModal from './RadarrModal';
interface ServerInstanceProps {
name: string;
isDefault?: boolean;
isDefault4K?: boolean;
address: string;
isSSL?: boolean;
profileName: string;
isSonarr?: boolean;
onEdit: () => void;
onDelete: () => void;
}
const ServerInstance: React.FC<ServerInstanceProps> = ({
name,
address,
profileName,
isDefault4K = false,
isDefault = false,
isSSL = false,
isSonarr = false,
onEdit,
onDelete,
}) => {
return (
<li className="col-span-1 bg-cool-gray-700 rounded-lg shadow">
<div className="w-full flex items-center justify-between p-6 space-x-6">
<div className="flex-1 truncate">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-white text-sm leading-5 font-medium truncate">
{name}
</h3>
{isDefault && <Badge>Default</Badge>}
{isDefault4K && <Badge badgeType="warning">Default 4K</Badge>}
{isSSL && <Badge badgeType="success">SSL</Badge>}
</div>
<p className="mt-1 text-cool-gray-300 text-sm leading-5 truncate">
<span className="font-bold mr-2">Address</span>
{address}
</p>
<p className="mt-1 text-cool-gray-300 text-sm leading-5 truncate">
<span className="font-bold mr-2">Active Profile</span> {profileName}
</p>
</div>
<img
className="w-10 h-10 bg-gray-300 rounded-full flex-shrink-0"
src={`/images/${isSonarr ? 'sonarr' : 'radarr'}_logo.png`}
alt=""
/>
</div>
<div className="border-t border-cool-gray-800">
<div className="-mt-px flex">
<div className="w-0 flex-1 flex border-r border-cool-gray-800">
<button
onClick={() => onEdit()}
className="relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4 text-sm leading-5 text-cool-gray-200 font-medium border border-transparent rounded-bl-lg hover:text-white focus:outline-none focus:shadow-outline-blue focus:border-cool-gray-500 focus:z-10 transition ease-in-out duration-150"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
<span className="ml-3">Edit</span>
</button>
</div>
<div className="-ml-px w-0 flex-1 flex">
<button
onClick={() => onDelete()}
className="relative w-0 flex-1 inline-flex items-center justify-center py-4 text-sm leading-5 text-cool-gray-200 font-medium border border-transparent rounded-br-lg hover:text-white focus:outline-none focus:shadow-outline-blue focus:border-cool-gray-500 focus:z-10 transition ease-in-out duration-150"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg>
<span className="ml-3">Delete</span>
</button>
</div>
</div>
</div>
</li>
);
};
const SettingsServices: React.FC = () => {
const {
data: radarrData,
error: radarrError,
revalidate: revalidateRadarr,
} = useSWR<RadarrSettings[]>('/api/v1/settings/radarr');
const { data: sonarrData, error: sonarrError } = useSWR<SonarrSettings[]>(
'/api/v1/settings/sonarr'
);
const [editRadarrModal, setEditRadarrModal] = useState<{
open: boolean;
radarr: RadarrSettings | null;
}>({
open: false,
radarr: null,
});
return (
<>
<div>
<h3 className="text-lg leading-6 font-medium text-cool-gray-200">
Radarr Settings
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-cool-gray-500">
Configure your Radarr connection below. You can have multiple Radarr
configurations but only two can be active as defaults at any time (one
for standard HD and one for 4K). Administrations can override a titles
connection to use in the manage title screen.
</p>
</div>
{editRadarrModal.open && (
<RadarrModal
radarr={editRadarrModal.radarr}
onClose={() => setEditRadarrModal({ open: false, radarr: null })}
onSave={() => {
revalidateRadarr();
setEditRadarrModal({ open: false, radarr: null });
}}
/>
)}
<div className="mt-6 sm:mt-5">
{!radarrData && !radarrError && <LoadingSpinner />}
{radarrData && !radarrError && (
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{radarrData.map((radarr) => (
<ServerInstance
key={`radarr-config-${radarr.id}`}
name={radarr.name}
address={radarr.hostname}
profileName={radarr.activeProfileName}
isSSL={radarr.useSsl}
isDefault={radarr.isDefault && !radarr.is4k}
isDefault4K={radarr.is4k && radarr.isDefault}
onEdit={() => setEditRadarrModal({ open: true, radarr })}
onDelete={() => console.log('delete clicked')}
/>
))}
<li className="col-span-1 border-2 border-dashed border-cool-gray-400 rounded-lg shadow h-32 sm:h-32">
<div className="flex items-center justify-center w-full h-full">
<Button
buttonType="ghost"
onClick={() =>
setEditRadarrModal({ open: true, radarr: null })
}
>
<svg
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
Add New Radarr Instance
</Button>
</div>
</li>
</ul>
)}
</div>
<div className="mt-10">
<h3 className="text-lg leading-6 font-medium text-cool-gray-200">
Sonarr Settings
</h3>
<p className="mt-1 max-w-2xl text-sm leading-5 text-cool-gray-500">
Configure your Sonarr connection below. You can have multiple Sonarr
configurations but only two can be active as defaults at any time (one
for standard HD and one for 4K). Administrations can override a titles
connection to use in the manage title screen.
</p>
</div>
<div className="mt-6 sm:mt-5">
{!sonarrData && !sonarrError && <LoadingSpinner />}
{sonarrData && !sonarrError && (
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{sonarrData.map((sonarr) => (
<ServerInstance
key={`sonarr-config-${sonarr.id}`}
name={sonarr.name}
address={sonarr.hostname}
profileName={sonarr.activeProfileId.toString()}
isSSL={sonarr.useSsl}
onEdit={() => console.log('nada')}
onDelete={() => console.log('delete clicked')}
/>
))}
<li className="col-span-1 border-2 border-dashed border-cool-gray-400 rounded-lg shadow h-32 sm:h-32">
<div className="flex items-center justify-center w-full h-full">
<Button
buttonType="ghost"
onClick={() =>
setEditRadarrModal({ open: true, radarr: null })
}
>
<svg
className="w-5 h-5 mr-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
Add New Sonarr Instance
</Button>
</div>
</li>
</ul>
)}
</div>
</>
);
};
export default SettingsServices;

@ -0,0 +1,14 @@
import React from 'react';
import type { NextPage } from 'next';
import SettingsLayout from '../../components/Settings/SettingsLayout';
import SettingsServices from '../../components/Settings/SettingsServices';
const ServicesSettingsPage: NextPage = () => {
return (
<SettingsLayout>
<SettingsServices />
</SettingsLayout>
);
};
export default ServicesSettingsPage;

@ -1059,6 +1059,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.5":
version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740"
integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.10.4", "@babel/template@^7.7.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
@ -1889,6 +1896,11 @@
resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.31.tgz#b1a620b115c96db7b3bfdf0cf54aee0c57139245"
integrity sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ==
"@types/yup@^0.29.9":
version "0.29.9"
resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.9.tgz#e2015187ae5739fd3b791b3b7ab9094f2aa5a474"
integrity sha512-ZtjjlrHuHTYctHDz3c8XgInjj0v+Hahe32N/4cDa2banibf9w6aAgxwx0jZtBjKKzmGIU4NXhofEsBW1BbqrNg==
"@typescript-eslint/eslint-plugin@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.0.0.tgz#99349a501447fed91de18346705c0c65cf603bee"
@ -5064,6 +5076,11 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
fn-name@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
fn.name@1.x.x:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
@ -6456,7 +6473,7 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.14:
lodash-es@^4.17.11, lodash-es@^4.17.14:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
@ -8309,6 +8326,11 @@ prop-types@15.7.2, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
property-expr@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
@ -9761,6 +9783,11 @@ swr@^0.3.5:
dependencies:
dequal "2.0.2"
synchronous-promise@^2.0.13:
version "2.0.15"
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.15.tgz#07ca1822b9de0001f5ff73595f3d08c4f720eb8e"
integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg==
table@^5.2.3:
version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
@ -9993,6 +10020,11 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@ -10771,3 +10803,16 @@ yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
yup@^0.29.3:
version "0.29.3"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea"
integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ==
dependencies:
"@babel/runtime" "^7.10.5"
fn-name "~3.0.0"
lodash "^4.17.15"
lodash-es "^4.17.11"
property-expr "^2.0.2"
synchronous-promise "^2.0.13"
toposort "^2.0.2"

Loading…
Cancel
Save