mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: radarr edit/create modal/backend functionality
This commit is contained in:
@@ -116,6 +116,9 @@ components:
|
|||||||
activeProfileId:
|
activeProfileId:
|
||||||
type: number
|
type: number
|
||||||
example: 1
|
example: 1
|
||||||
|
activeProfileName:
|
||||||
|
type: string
|
||||||
|
example: 720p/1080p
|
||||||
activeDirectory:
|
activeDirectory:
|
||||||
type: string
|
type: string
|
||||||
example: '/movies'
|
example: '/movies'
|
||||||
@@ -125,16 +128,21 @@ components:
|
|||||||
minimumAvailability:
|
minimumAvailability:
|
||||||
type: string
|
type: string
|
||||||
example: 'In Cinema'
|
example: 'In Cinema'
|
||||||
|
isDefault:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- hostname
|
- hostname
|
||||||
- port
|
- port
|
||||||
- apiKey
|
- apiKey
|
||||||
- useSsl
|
- useSsl
|
||||||
- activeProfile
|
- activeProfileId
|
||||||
|
- activeProfileName
|
||||||
- activeDirectory
|
- activeDirectory
|
||||||
- is4k
|
- is4k
|
||||||
- minimumAvailability
|
- minimumAvailability
|
||||||
|
- isDefault
|
||||||
SonarrSettings:
|
SonarrSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -162,12 +170,18 @@ components:
|
|||||||
activeProfileId:
|
activeProfileId:
|
||||||
type: number
|
type: number
|
||||||
example: 1
|
example: 1
|
||||||
|
activeProfileName:
|
||||||
|
type: string
|
||||||
|
example: 720p/1080p
|
||||||
activeDirectory:
|
activeDirectory:
|
||||||
type: string
|
type: string
|
||||||
example: '/tv/'
|
example: '/tv/'
|
||||||
activeAnimeProfileId:
|
activeAnimeProfileId:
|
||||||
type: number
|
type: number
|
||||||
nullable: true
|
nullable: true
|
||||||
|
activeAnimeProfileName:
|
||||||
|
type: string
|
||||||
|
example: 720p/1080p
|
||||||
activeAnimeDirectory:
|
activeAnimeDirectory:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -177,6 +191,9 @@ components:
|
|||||||
enableSeasonFolders:
|
enableSeasonFolders:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
|
isDefault:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
- hostname
|
- hostname
|
||||||
@@ -184,9 +201,11 @@ components:
|
|||||||
- apiKey
|
- apiKey
|
||||||
- useSsl
|
- useSsl
|
||||||
- activeProfileId
|
- activeProfileId
|
||||||
|
- activeProfileName
|
||||||
- activeDirectory
|
- activeDirectory
|
||||||
- is4k
|
- is4k
|
||||||
- enableSeasonFolders
|
- enableSeasonFolders
|
||||||
|
- isDefault
|
||||||
PublicSettings:
|
PublicSettings:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -704,6 +723,15 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
twitterId:
|
twitterId:
|
||||||
type: string
|
type: string
|
||||||
|
ServiceProfile:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
example: 1
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: 720p/1080p
|
||||||
|
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
cookieAuth:
|
cookieAuth:
|
||||||
@@ -877,6 +905,50 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/RadarrSettings'
|
$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}:
|
/settings/radarr/{radarrId}:
|
||||||
put:
|
put:
|
||||||
summary: Update existing radarr instance
|
summary: Update existing radarr instance
|
||||||
@@ -922,6 +994,28 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/RadarrSettings'
|
$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:
|
/settings/sonarr:
|
||||||
get:
|
get:
|
||||||
summary: Get all sonarr settings
|
summary: Get all sonarr settings
|
||||||
|
@@ -45,7 +45,8 @@
|
|||||||
"uuid": "^8.3.0",
|
"uuid": "^8.3.0",
|
||||||
"winston": "^3.3.3",
|
"winston": "^3.3.3",
|
||||||
"xml2js": "^0.4.23",
|
"xml2js": "^0.4.23",
|
||||||
"yamljs": "^0.3.0"
|
"yamljs": "^0.3.0",
|
||||||
|
"yup": "^0.29.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.11.6",
|
"@babel/cli": "^7.11.6",
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@types/xml2js": "^0.4.5",
|
"@types/xml2js": "^0.4.5",
|
||||||
"@types/yamljs": "^0.2.31",
|
"@types/yamljs": "^0.2.31",
|
||||||
|
"@types/yup": "^0.29.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||||
"@typescript-eslint/parser": "^3.10.1",
|
"@typescript-eslint/parser": "^3.10.1",
|
||||||
"babel-plugin-react-intl": "^8.2.2",
|
"babel-plugin-react-intl": "^8.2.2",
|
||||||
|
BIN
public/images/radarr_logo.png
Normal file
BIN
public/images/radarr_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
public/images/sonarr_logo.png
Normal file
BIN
public/images/sonarr_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@@ -25,8 +25,10 @@ interface DVRSettings {
|
|||||||
useSsl: boolean;
|
useSsl: boolean;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
activeProfileId: number;
|
activeProfileId: number;
|
||||||
|
activeProfileName: string;
|
||||||
activeDirectory: string;
|
activeDirectory: string;
|
||||||
is4k: boolean;
|
is4k: boolean;
|
||||||
|
isDefault: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RadarrSettings extends DVRSettings {
|
export interface RadarrSettings extends DVRSettings {
|
||||||
@@ -35,6 +37,7 @@ export interface RadarrSettings extends DVRSettings {
|
|||||||
|
|
||||||
export interface SonarrSettings extends DVRSettings {
|
export interface SonarrSettings extends DVRSettings {
|
||||||
activeAnimeProfileId?: number;
|
activeAnimeProfileId?: number;
|
||||||
|
activeAnimeProfileName?: string;
|
||||||
activeAnimeDirectory?: string;
|
activeAnimeDirectory?: string;
|
||||||
enableSeasonFolders: boolean;
|
enableSeasonFolders: boolean;
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,9 @@ import { getRepository } from 'typeorm';
|
|||||||
import { User } from '../entity/User';
|
import { User } from '../entity/User';
|
||||||
import PlexAPI, { PlexLibrary } from '../api/plexapi';
|
import PlexAPI, { PlexLibrary } from '../api/plexapi';
|
||||||
import jobPlexSync from '../job/plexsync';
|
import jobPlexSync from '../job/plexsync';
|
||||||
|
import SonarrAPI from '../api/sonarr';
|
||||||
|
import RadarrAPI from '../api/radarr';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
const settingsRoutes = Router();
|
const settingsRoutes = Router();
|
||||||
|
|
||||||
@@ -132,6 +135,35 @@ settingsRoutes.post('/radarr', (req, res) => {
|
|||||||
return res.status(201).json(newRadarr);
|
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) => {
|
settingsRoutes.put<{ id: string }>('/radarr/:id', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
@@ -154,6 +186,36 @@ settingsRoutes.put<{ id: string }>('/radarr/:id', (req, res) => {
|
|||||||
return res.status(200).json(settings.radarr[radarrIndex]);
|
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) => {
|
settingsRoutes.delete<{ id: string }>('/radarr/:id', (req, res) => {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
|
@@ -5,7 +5,8 @@ export type ButtonType =
|
|||||||
| 'primary'
|
| 'primary'
|
||||||
| 'danger'
|
| 'danger'
|
||||||
| 'warning'
|
| 'warning'
|
||||||
| 'success';
|
| 'success'
|
||||||
|
| 'ghost';
|
||||||
|
|
||||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
buttonType?: ButtonType;
|
buttonType?: ButtonType;
|
||||||
@@ -30,22 +31,27 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
break;
|
break;
|
||||||
case 'danger':
|
case 'danger':
|
||||||
buttonStyle.push(
|
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;
|
break;
|
||||||
case 'warning':
|
case 'warning':
|
||||||
buttonStyle.push(
|
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;
|
break;
|
||||||
case 'success':
|
case 'success':
|
||||||
buttonStyle.push(
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
buttonStyle.push(
|
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 && (
|
item && (
|
||||||
<animated.div
|
<animated.div
|
||||||
style={props}
|
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"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-headline"
|
aria-labelledby="modal-headline"
|
||||||
@@ -116,7 +116,7 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
{iconSvg}
|
{iconSvg}
|
||||||
</div>
|
</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 && (
|
{title && (
|
||||||
<h3
|
<h3
|
||||||
className="text-lg leading-6 font-medium text-white"
|
className="text-lg leading-6 font-medium text-white"
|
||||||
@@ -128,10 +128,8 @@ const Modal: React.FC<ModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{children && (
|
{children && (
|
||||||
<div className="mt-4">
|
<div className="mt-4 text-sm leading-5 text-cool-gray-300">
|
||||||
<p className="text-sm leading-5 text-cool-gray-300">
|
|
||||||
{children}
|
{children}
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(onCancel || onOk || onSecondary || onTertiary) && (
|
{(onCancel || onOk || onSecondary || onTertiary) && (
|
||||||
|
@@ -8,6 +8,7 @@ import { useUser, Permission } from '../../../hooks/useUser';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
dashboard: 'Dashboard',
|
dashboard: 'Dashboard',
|
||||||
requests: 'Requests',
|
requests: 'Requests',
|
||||||
|
users: 'Users',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,6 +69,22 @@ const SidebarLinks: SidebarLinkProps[] = [
|
|||||||
),
|
),
|
||||||
activeRegExp: /^\/requests/,
|
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',
|
href: '/settings',
|
||||||
messagesKey: 'settings',
|
messagesKey: 'settings',
|
||||||
|
464
src/components/Settings/RadarrModal.tsx
Normal file
464
src/components/Settings/RadarrModal.tsx
Normal file
@@ -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>
|
</p>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className="bg-cool-gray-800 p-4 rounded-md">
|
<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 && (
|
{dataSync?.running && (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
width: `${Math.round(
|
width: `${Math.round(
|
||||||
(dataSync.progress / dataSync.total) * 100
|
(dataSync.progress / dataSync.total) * 100
|
||||||
|
247
src/components/Settings/SettingsServices.tsx
Normal file
247
src/components/Settings/SettingsServices.tsx
Normal file
@@ -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;
|
14
src/pages/settings/services.tsx
Normal file
14
src/pages/settings/services.tsx
Normal file
@@ -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;
|
47
yarn.lock
47
yarn.lock
@@ -1059,6 +1059,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
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":
|
"@babel/template@^7.10.4", "@babel/template@^7.7.4":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.31.tgz#b1a620b115c96db7b3bfdf0cf54aee0c57139245"
|
||||||
integrity sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ==
|
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":
|
"@typescript-eslint/eslint-plugin@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.0.0.tgz#99349a501447fed91de18346705c0c65cf603bee"
|
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"
|
inherits "^2.0.3"
|
||||||
readable-stream "^2.3.6"
|
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:
|
fn.name@1.x.x:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
|
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
|
||||||
@@ -6456,7 +6473,7 @@ locate-path@^6.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^5.0.0"
|
p-locate "^5.0.0"
|
||||||
|
|
||||||
lodash-es@^4.17.14:
|
lodash-es@^4.17.11, lodash-es@^4.17.14:
|
||||||
version "4.17.15"
|
version "4.17.15"
|
||||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||||
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
|
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"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.8.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:
|
proxy-addr@~2.0.5:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
||||||
@@ -9761,6 +9783,11 @@ swr@^0.3.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dequal "2.0.2"
|
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:
|
table@^5.2.3:
|
||||||
version "5.4.6"
|
version "5.4.6"
|
||||||
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
|
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"
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||||
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
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:
|
touch@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
|
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
|
||||||
@@ -10771,3 +10803,16 @@ yn@3.1.1:
|
|||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
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"
|
||||||
|
Reference in New Issue
Block a user